Compare commits

...

227 Commits
api ... master

Author SHA1 Message Date
solis 0550222eed
fixed non working english letters 2018-12-10 04:16:26 +10:30
Sunpy a0053de311 Updated submodule 2018-10-09 11:02:49 +02:00
Sunpy 848dacf621 Reworked config structure 2018-10-09 10:54:35 +02:00
Sunpy 5111b6b05f Updated submodule 2018-10-09 10:53:26 +02:00
Sunpy 9ad8d5779e Dirty fix for match size init 2018-09-10 23:35:59 +02:00
Sunpy 71f5a8c4d0 THEY!!! IT IS NOT HARD! 2018-09-10 22:45:03 +02:00
Sunpy 768971e240 Updated submodules 2018-09-02 01:45:33 +02:00
Giuseppe Guerra a248b4032c Opsie wopsie 2018-08-14 00:04:46 +02:00
Giuseppe Guerra cb6d828fd9 Add !bloodcat command, automatically send bloodcat links in #spectator 2018-08-14 00:04:04 +02:00
Giuseppe Guerra 925d55bb16 🔼 1.13.7 🔼 2018-08-12 17:19:12 +02:00
Giuseppe Guerra 4e97c717f5 Properly handle exceptions in periodic loops 2018-08-12 17:18:12 +02:00
Giuseppe Guerra f11708d463 Removed useless log 2018-08-12 17:08:43 +02:00
Giuseppe Guerra 4c3427fd76 Read main menu icon from main_menu_icons table 2018-08-12 17:03:18 +02:00
Giuseppe Guerra a3ab53e108 🔼 1.13.6 🔼 2018-08-04 23:11:26 +02:00
Giuseppe Guerra 74cfadb261 Fix uncaught userAlreadyInChannelException in channel __init__ 2018-08-04 23:11:07 +02:00
Giuseppe Guerra c09c7b5d96 ??? 2018-08-04 23:10:38 +02:00
Giuseppe Guerra 56c1dcc48a 🔼 1.13.5 🔼 2018-08-04 20:13:30 +02:00
Giuseppe Guerra 1b9224fc79 Fix broken sentry client 2018-08-04 19:58:22 +02:00
Giuseppe Guerra 887836045d Send match history link after the first match 2018-08-04 19:49:38 +02:00
Giuseppe Guerra cb2d1e74f7 The ascension of the Madonna (the 15th of August is approaching) 2018-08-04 19:26:42 +02:00
Giuseppe Guerra 7f283d9aa2 Forbid #spect_ and #multi_ manual join from game clients 2018-08-04 19:16:40 +02:00
Giuseppe Guerra 333cfca806 Professional debuggingTM 2018-08-04 19:13:29 +02:00
Giuseppe Guerra 1dc37ecb77 Send alert when doing !mp join from an IRC client 2018-08-04 18:53:04 +02:00
Giuseppe Guerra a456508ed3 Add !mp settings single 2018-08-04 18:52:48 +02:00
Giuseppe Guerra 66acc12099 Add !mp scorev and !mp help 2018-08-04 18:47:31 +02:00
Giuseppe Guerra a9fc15d524 Add support for EZ in !mp mods 2018-08-04 18:36:32 +02:00
Giuseppe Guerra 02b08d3024 Add AGPL license agreement 2018-07-13 12:30:23 +02:00
Giuseppe Guerra d8c6fa4993 Updated submodule common 2018-07-13 12:28:20 +02:00
Giuseppe Guerra fe05aa7ace 🔼 1.13.4 🔼 2018-07-04 17:26:58 +02:00
Giuseppe Guerra 79ecdbcd44 Deprecated telegram 2fa 2018-07-04 17:01:22 +02:00
Giuseppe Guerra fd47bc6e32 Updated submodule common 2018-07-04 17:01:06 +02:00
Giuseppe Guerra 1a4e952e4f Ignore .pyx files in .pyenv 2018-04-13 23:38:05 +02:00
Sunpy 2cd69a9a63 Merged with upstream 2018-04-08 21:20:26 +02:00
Giuseppe Guerra ea4e2bd4fd Pin cython to 0.27.3 2018-04-03 12:40:45 +02:00
Giuseppe Guerra 508f6b507a Add .pyenv to .gitignore 2018-04-03 12:38:32 +02:00
Giuseppe Guerra e89fbe7604 Pin requirements 2018-04-03 12:37:29 +02:00
Giuseppe Guerra 05694c5d87 Handle invalidUserException when calling fokabot commands 2018-04-02 18:19:47 +02:00
Giuseppe Guerra 198bdb9997 * 2018-04-02 12:15:22 +02:00
Giuseppe Guerra 473a2e1f2d 🔼 1.13.3 🔼 2018-03-23 21:38:24 +01:00
Giuseppe Guerra a809008e55 Improvements to fokabot commands 2018-03-23 21:37:47 +01:00
Giuseppe Guerra 201593ea02 Handle empty arguments in fokabot !mp commands 2018-03-23 21:28:24 +01:00
Giuseppe Guerra 3275d31861 Handle matches with no name 2018-03-23 21:28:13 +01:00
Giuseppe Guerra 05eead0e1a Handle empty chat messages 2018-03-23 21:27:48 +01:00
Giuseppe Guerra 8bdc56faf6 🔼 1.13.2 🔼 2018-03-23 21:05:06 +01:00
Giuseppe Guerra 745a833aab Fix !mp close not working 2018-03-23 21:03:27 +01:00
Giuseppe Guerra dcad5c5736 git.zxq.co -> zxq.co 2018-03-23 20:59:44 +01:00
Giuseppe Guerra c1f8ca8ed3 Removed timeoutTime and checkTime args from usersTimeoutCheckLoop 2018-03-23 20:59:30 +01:00
Giuseppe Guerra e0d54f49d1 Add empty match cleanup job 2018-03-23 20:59:04 +01:00
Sunpy c2af1b9772 Updated submodules 2018-02-16 20:12:52 +01:00
Sunpy a99bf0c74b Unhardcoded hardcoded code :) 2018-02-14 20:03:36 +01:00
Sunpy f0fa00b181 Additional configuration 2018-02-14 19:59:43 +01:00
Sunpy 956aa8161f Updated submodules 2018-02-14 18:31:28 +01:00
Sunpy 08b367812f Use constant values in commands 2018-02-14 18:16:09 +01:00
Sunpy 0722c91018 Fixed mpClose function return message 2018-02-14 17:53:14 +01:00
Sunpy f0e8223b5c Updated submodules 2018-02-14 17:52:27 +01:00
Sunpy 27a5f9c000 Use whatever account that is in id 999 as bot 2018-02-14 17:44:37 +01:00
Sunpy 29db61fd12 Updated submodules 2018-02-12 21:16:33 +01:00
Giuseppe Guerra e63a85e4a4 I knew a threaded bancho server was going to be a bad idea... 2017-12-21 18:58:56 +01:00
Giuseppe Guerra 050c1d5fe8 Fix AiAe.exe willing to break stuff 2017-12-08 13:08:54 +01:00
Giuseppe Guerra 90cd4634fd Le melanzane fritte sono poco digeribili 2017-12-05 21:17:09 +01:00
Giuseppe Guerra e3f1bc05e9 Changed !rtx privileges to ADMIN_KICK_USERS 2017-12-04 23:34:04 +01:00
Giuseppe Guerra 0939ec972c Merge remote-tracking branch 'origin/master' 2017-10-31 13:11:21 +01:00
Giuseppe Guerra 2ef89daf4c Force ASCII encoding in /api/v1/fokabotMessage 2017-10-31 13:11:01 +01:00
Morgan Bazalgette bb8ccf8c85
make chat mods red, too 2017-10-09 15:24:34 +02:00
Giuseppe Guerra dc90f506bd Fix np bug again 2017-09-22 22:57:21 +02:00
Giuseppe Guerra 2f9179362c Fix !last not returning pp for taiko scores 2017-09-22 22:46:40 +02:00
Giuseppe Guerra 3dfb1228ee Merge branch 'tourney-mp-room-changes' of ripple/pep.py into master 2017-09-09 12:48:30 +02:00
Morgan Bazalgette c4123eb636
fix positional argument exception meme 2017-09-09 12:42:49 +02:00
Morgan Bazalgette e3e46a34ec
Notify the chat when the match has been completed. 2017-09-09 12:30:25 +02:00
Morgan Bazalgette 3ed837dc96
In tourney rooms, send a message in the chat when the ready status changes. 2017-09-09 12:25:51 +02:00
Nyo 48534bb551 Fix typo 🤔 2017-08-26 22:28:05 +02:00
goeo_ 36d701eac1 Make the irc gateway not receive the last empty line on multiline messages. 2017-08-20 18:01:18 +03:00
goeo_ 60fe2bc56e Make !mp close close the match even if the user is only in the match's channel and not in the match 2017-08-20 17:22:47 +03:00
goeo_ b8baddf694 Make the irc gateway compatible with multi-line messages 2017-08-20 17:10:15 +03:00
Giuseppe Guerra at an airport 018da5c0a2 Removed chat filters because goeo is a dickhead 2017-08-15 08:43:33 +02:00
Giuseppe Guerra at an airport 0a2ca07198 Bump version 2017-08-12 21:36:16 +02:00
Giuseppe Guerra at an airport 34e7a332e6 Add spect lock back 2017-08-12 21:29:27 +02:00
Giuseppe Guerra 8d97227965 Fix race condition while iterating over clients 2017-08-12 19:07:28 +02:00
Giuseppe Guerra at an airport 176775f8f3 Remove extra locks 2017-08-12 18:56:31 +02:00
Giuseppe Guerra at an airport 0329847477 Don't start spectating unexisting users if user id is negative 2017-08-12 08:56:39 +02:00
Giuseppe Guerra at an airport b24b4ee88d Remove _streamLock 2017-08-12 08:49:55 +02:00
Giuseppe Guerra e40acd335b Fix some commands not being triggered 2017-08-11 23:17:33 +02:00
Giuseppe Guerra ce75f5ee99 Remove _chatLock and _internalLock 2017-08-11 22:19:39 +02:00
Giuseppe Guerra 29359ad4fd Fix various bugs with multiplayer teams and mp commands 2017-08-11 22:04:25 +02:00
Giuseppe Guerra def4891008 Fix wrong commands being triggered if they have the same letters at the beginning 2017-08-11 01:36:25 +02:00
Giuseppe Guerra 5d73682a71 Fix username with spaces breaking commands 2017-08-11 01:33:14 +02:00
Giuseppe Guerra 0229fc4e65 Mp commands now require USER_TOURNAMENT_STAFF privilege 2017-08-11 01:19:30 +02:00
Giuseppe Guerra 05c4c89002 Merge branch 'master' of git.zxq.co:ripple/pep.py 2017-08-11 01:14:45 +02:00
Giuseppe Guerra 02b266f229 Add !mp settings 2017-08-11 01:14:21 +02:00
Giuseppe Guerra 612e808702 Add !mp start force, start game even for non-ready players, check if everyone is ready in !mp start 2017-08-11 00:57:58 +02:00
Giuseppe Guerra 466004f239 Add various locks in osuToken object 2017-08-11 00:45:44 +02:00
goeo_ 5f279da6cf Send the api fail info and teams too when a mp match finishes. 2017-08-10 15:53:18 -04:00
Giuseppe Guerra aa1887e2c4 Fix match teams not being changed 2017-08-08 00:56:39 +02:00
Giuseppe Guerra df2a9bb13d Bump version to 1.13.0 2017-08-07 23:53:21 +02:00
Giuseppe Guerra e2149d7d61 Fix wrong variable names 2017-08-07 23:53:09 +02:00
Giuseppe Guerra ffc84448a2 Fix non-working packet 93, allow tournament-only packets only from tourney clients 2017-08-07 23:39:13 +02:00
Giuseppe Guerra 933c92e5f2 Allow tourney clients to send messages in chat, send match info to lobby after creating it with !mp make 2017-08-07 23:35:33 +02:00
Giuseppe Guerra 3bb1029832 Add !mp team 2017-08-07 23:21:49 +02:00
Giuseppe Guerra ce889e608a Add !mp mods 2017-08-07 22:30:44 +02:00
Giuseppe Guerra aeecccdd13 Add !mp password and !mp randompassword 2017-08-07 22:14:44 +02:00
Giuseppe Guerra 9425043b1e Add !mp kick 2017-08-07 21:48:09 +02:00
Giuseppe Guerra f660a20f60 Changed !mp start countdown message 2017-08-07 21:41:48 +02:00
Giuseppe Guerra 5adc7f4261 Add !mp abort, fix match not being set as in progress when starting it 2017-08-07 21:38:18 +02:00
Giuseppe Guerra 3653447761 Add !mp set 2017-08-07 20:51:16 +02:00
Giuseppe Guerra 17aab9551e Add !mp map 2017-08-06 10:09:05 +02:00
Giuseppe Guerra 87d6186993 Add !mp invite 2017-08-06 09:45:39 +02:00
Giuseppe Guerra 5975e84f52 Add !mp start 2017-08-04 01:04:26 +02:00
Giuseppe Guerra 3309f2f8fd Add !mp host and !mp clearhost 2017-08-04 00:24:12 +02:00
Giuseppe Guerra baa8ae4cc2 Changes !mp clear to !mp close 2017-08-04 00:10:26 +02:00
Giuseppe Guerra 837df03f05 Fix typo 2017-08-04 00:09:50 +02:00
Giuseppe Guerra c2a2d9c97b Change minimum !mp size value from 1 to 2 2017-08-03 23:56:04 +02:00
Giuseppe Guerra fc989a2705 Fix tourney chat channels get deleted when there's nobody in the channel 2017-08-03 23:55:26 +02:00
Giuseppe Guerra 310bc1d3b3 Add !mp size 2017-08-03 23:47:34 +02:00
Giuseppe Guerra 9f647d5f9e Add !switchserver command 2017-08-02 01:36:37 +02:00
Giuseppe Guerra 1e6ee91685 Add !mp lock and !mp unlock 2017-08-02 00:39:16 +02:00
Giuseppe Guerra 86995feb34 Alert users when entering tournament matches, make impossible to join tournament matches from ui 2017-08-02 00:29:12 +02:00
Giuseppe Guerra af554c94d5 Add !mp make and !mp join 2017-08-02 00:22:57 +02:00
Giuseppe Guerra 3373bc9581 Fix foka responding to commands even if the command is not at the beginning of the message 2017-08-02 00:02:46 +02:00
goeo_ 2cf1cdf1fd Send the api some data when a multiplayer match ends 2017-07-31 17:54:50 -04:00
Giuseppe Guerra 8c3fc6842d Release locks properly 2017-07-28 22:46:29 +02:00
Giuseppe Guerra eab8bee828 Merge branch 'master' of git.zxq.co:ripple/pep.py 2017-07-28 22:36:19 +02:00
Giuseppe Guerra 66061d5fb2 Lock client buffers while writing 2017-07-28 22:32:30 +02:00
goeo_ 401dd5ecdb Fix the multiplayer password bug while not breaking anything else, especially not multiplayer as a whole. (tested code) 2017-07-23 14:37:12 -04:00
goeo_ d439490029 I should get a proper development environment 2017-07-23 12:21:38 +02:00
goeo_ f4d0a2424d .HIDE. I suck at coding
does the hide tag even work lol
2017-07-23 12:17:57 +02:00
goeo_ a177e65fcf Silence check is already done in bancho; made the ban check look better
weirdly, the original bug report said silenced people could talk using irc
2017-07-23 12:16:29 +02:00
goeo_ c14c86fe0d Fixed The Underscore Bug (i think) 2017-07-23 03:40:51 +02:00
goeo_ 25df2228e3 Finally tested the code and it works now 2017-07-23 02:58:47 +02:00
goeo_ 941cf81877 Parenthesis
haha coding on windows sucks because the easiest way to test the code is to push the code then pull it from my dev server
2017-07-23 02:38:37 +02:00
goeo_ 76bb15f9f3 Banned/Restricted people can't use the IRC gateway. Silenced people can't send messages on it. 2017-07-23 02:35:29 +02:00
goeo_ 8043d686c5 Fix all multi passwords being sent to each user in lobby 2017-07-22 22:41:42 +02:00
Nyo cae82bd107 Delete timed out tokens 2017-07-04 23:16:10 +02:00
goeo_ 2c2c85b382 Trying to fix the user metadata problem. Will this kill performance? 2017-04-16 06:41:25 -04:00
Giuseppe Guerra a0fdc6c292 1.12.1 2017-04-13 17:22:55 +02:00
Giuseppe Guerra 487f583083 Update submodules 2017-04-13 17:22:35 +02:00
Giuseppe Guerra 530d0c3b74 Update submodules 2017-04-12 21:11:05 +02:00
Giuseppe Guerra 46ec4f3704 Merge branch 'master' of zxq.co:ripple/pep.py 2017-04-11 18:18:19 +02:00
Giuseppe Guerra 0aa0ab0475 Update submodules, moved global leaderboards to redis 2017-04-11 18:18:07 +02:00
Giuseppe Guerra 1c2a29a88e Merge branch 'master' of AiAe/pep.py into master 2017-04-10 20:02:47 +02:00
Daniel 92e57aff28 Update 'filters.txt' 2017-04-10 19:21:41 +02:00
Giuseppe Guerra 7e535d7ed5 Remove peppy->nyo filter 2017-04-08 19:21:49 +02:00
Giuseppe Guerra 0464f713f7 Merge branch 'master' of AiAe/pep.py into master 2017-04-08 19:21:04 +02:00
Daniel 66776c60e0 ¯\_(ツ)_/¯ 2017-04-08 19:11:05 +02:00
Morgan Bazalgette 4ef98b5fc0 Howl porcoddio l'indentazione 2017-02-16 22:11:15 +01:00
Morgan Bazalgette 96add06b44 Add some more FAQs as requested by AiAe 2017-02-16 22:10:03 +01:00
Nyo 31971d4a8b .HIDE. Update submodules 2017-02-03 21:46:31 +01:00
Nyo 9175f9e7f2 .BANCHO. .FIX. Fix utf-8 characters not being displayed correctly in chatlogs 2017-01-17 19:24:12 +01:00
Nyo 784c4a11f1 .BANCHO. Switch from mirrorapi to cheesegull for beatmap update requests 2017-01-16 21:24:15 +01:00
Nyo 2407ecc3bf .HIDE. Change submodule url to new git server and use HTTPS instead of SSH 2017-01-16 19:39:54 +01:00
Nyo 9880c5004d .BANCHO. .FIX. Chat mods can now write in moderated channel and access #admin channel 2017-01-16 19:37:41 +01:00
Nyo d2f111fd7b .HIDE. Update submodules 2017-01-10 19:55:43 +01:00
Nyo e489221e39 .HIDE. Update submodules 2017-01-06 12:56:32 +01:00
Nyo 84f1fb566c .BANCHO. Removed dashes in when tracking notes for CM 2017-01-06 12:55:50 +01:00
Nyo fc3736eba8 .BANCHO. Remove double prefix on bancho start schiavo message 2017-01-06 12:23:45 +01:00
Nyo 768913da59 .BANCHO. Allow !last only in PM 2017-01-06 12:01:07 +01:00
Nyo c6417c31ed ⬆️ v1.11.2 ⬆️ 2017-01-06 11:48:59 +01:00
Nyo 9055fcaf5e .BANCHO. .FIX. Fix some format errors 2016-12-28 16:15:07 +01:00
Nyo f85640ae39 .HIDE. Update landscape config file 2016-12-28 16:12:39 +01:00
Nyo 18a7c47db6 .BANCHO. Add tornado-sentry capture decorators 2016-12-28 12:41:24 +01:00
Nyo 22ae4c332b .BANCHO. Log username changes to user notes in RAP 2016-12-28 12:16:46 +01:00
Nyo 8f156a0702 .HIDE. Update submodules 2016-12-26 10:33:39 +01:00
Nyo 62b67da9fb .HIDE. General refactoring 2016-12-26 10:33:05 +01:00
Nyo 501130721d .HIDE. Update submodules 2016-12-26 10:00:38 +01:00
Nyo e6cdef4580 .BANCHO. Removed some schiavo logs 2016-12-26 09:59:33 +01:00
Nyo 00c544b7c7 .BANCHO. Removed some schiavo logs 2016-12-26 09:52:43 +01:00
Nyo ebf0e1d458 .BANCHO. .FIX. Fix stop spectating not working properly if host disconnects 2016-12-21 23:26:47 +01:00
Nyo fd23cf2b2c ⬆️ v1.11.1 ⬆️ 2016-12-21 18:20:55 +01:00
Nyo 8a8a4968a3 .BANCHO. .FIX. Fix streams and temporary chat channels not being disposed correctly 2016-12-21 18:17:29 +01:00
Nyo 2ae3c5f701 .BANCHO. Add redis notifications pubsub handler 2016-12-20 21:22:25 +01:00
Nyo f4c099c809 .BANCHO. .FIX. Remove extra characters from version 2016-12-20 20:39:29 +01:00
Nyo f8cc0c738c Add code health badge in README 2016-12-17 15:33:12 +01:00
Nyo 4557b08df8 Add landscape config file 2016-12-17 15:28:55 +01:00
Nyo 49f8bd8cf1 .BANCHO. .HIDE. Antiamo a kuartare i kantieri koi vekki............... 2016-12-12 22:59:58 +01:00
Nyo 7ba5db62b4 .BANCHO. Report result is now a notification rather than a FokaBot message 2016-12-12 22:57:00 +01:00
Nyo 7f534f0984 .BANCHO. .FIX. Fix hour in user report chatlog 2016-12-12 22:45:31 +01:00
Nyo daf457fc5c .BANCHO. Add !report command 2016-12-11 23:12:06 +01:00
Nyo b4d498c26c .BANCHO. Kick all clients when using !kick, don't kick FokaBot with !kick command 2016-12-11 11:39:01 +01:00
Nyo 44545c3bcb .BANCHO. Use streams for public chat 2016-12-11 11:07:35 +01:00
Nyo c4a6c84cec .BANCHO. .HIDE. General refactoring 2016-12-10 19:31:12 +01:00
Nyo 8532731f19 .BANCHO. .FIX. Run safeUsername on /api/v1/isOnline 2016-12-10 19:30:38 +01:00
Nyo b836f77446 .BANCHO. .FIX. Fix !kick command not working on irc clients 2016-12-10 19:10:00 +01:00
Nyo e92cbe47bd ⬆️ 1.11.0 ⬆️ 2016-12-09 13:19:04 +01:00
Nyo 6ca2016f7b .BANCHO. Disabled datadog ram usage tracking 2016-12-09 11:47:00 +01:00
Nyo 2f54a56b7a Add full build script 2016-12-08 15:46:21 +01:00
Nyo cf9e506875 .HIDE. Update submodules 2016-12-08 12:04:59 +01:00
Nyo 5c93d692ea .BANCHO. Cythonized mainHandler 2016-12-08 11:44:27 +01:00
Nyo a8a1dfb1bc .HIDE. Update .gitignore 2016-12-08 11:44:12 +01:00
Nyo 9d562e7acd .BANCHO. Dynamic setup.py file 2016-12-08 11:43:23 +01:00
Nyo 4f4253afce .HIDE. Update README.txt 2016-12-07 22:31:28 +01:00
Nyo 04898c24ae .BANCHO. Ported packet encoder/decoder to Cython, add distutils setup file, update .gitignore, README and requirements.txt 2016-12-07 22:25:16 +01:00
Nyo 1b94936092 .BANCHO. .FIX. Fix wrong default configuration file 2016-12-07 21:15:55 +01:00
Nyo d4591b42a3 .BANCHO. !kickall command now requires 'manage server' privilege 2016-12-07 21:15:23 +01:00
Nyo 69508f9a0e Add Google auth 2fa check at login 2016-11-30 23:33:56 +01:00
Nyo 5cf8c1bde8 Merge branch 'master' of git.zxq.co:ripple/pep.py 2016-11-30 20:08:54 +01:00
Nyo 20be60d9db Update submodules 2016-11-30 20:08:43 +01:00
Howl 61935f323c add link to github mirror 2016-11-29 17:21:24 +01:00
Nyo cecef18d13 .HIDE. Update submodules 2016-11-20 14:17:35 +01:00
Nyo 5723c0e68f .BANCHO. Move online users count to redis 2016-11-20 14:17:05 +01:00
Nyo 525235a27e .BANCHO. Move bancho sessions to redis 2016-11-20 13:03:07 +01:00
Nyo 3bc390e3e6 .HIDE. Update submodules 2016-11-20 12:32:45 +01:00
Nyo f6ae673401 .HIDE. Update submodules 2016-11-20 11:32:21 +01:00
Nyo aa32e8bea6 .BANCHO. Add pubsub handlers for username changes, bans, restrictions, silences, stats update, kicks and bancho settings reload. 2016-11-20 11:31:51 +01:00
Nyo fb00063e0f ⬆️ v1.10.0 ⬆️ 2016-11-17 20:13:36 +01:00
Nyo e30893d66f .BANCHO. Add lets version in !system status 2016-11-17 20:11:11 +01:00
Nyo 8078596a0a .HIDE. Removed shrug version 2016-11-17 20:10:32 +01:00
Nyo eefec1f47b .BANCHO. Save pep.py version in redis 2016-11-17 20:07:06 +01:00
Nyo 2992dc6190 .HIDE. Update submodules 2016-11-17 19:39:50 +01:00
Nyo b6e2319e8c .HIDE. Remove random prints, fix #9 2016-11-17 19:15:14 +01:00
Nyo a2ef03c887 .HIDE. General refactoring and documentation 2016-11-17 19:13:06 +01:00
Nyo abad698fe3 .HIDE. avail pls 2016-11-17 15:28:07 +01:00
Nyo 030d556b9c .BANCHO. /api/v1/isOnline now supports both safe and unsafe usernames 2016-11-17 15:27:27 +01:00
Nyo d51b304fbe .HIDE. Update submodules 2016-11-17 13:39:57 +01:00
Nyo 1ecc73e0dc .HIDE. Update submodules 2016-11-16 23:22:25 +01:00
Nyo b1315815b2 Merge branch 'master' of git.zxq.co:ripple/pep.py 2016-11-15 20:38:58 +01:00
Nyo 38bcf3a735 .BANCHO. Add redis support, remove userID cache 2016-11-15 20:38:15 +01:00
Nyo a6292c7374 Add redis support, remove userID cache 2016-11-15 20:36:29 +01:00
Nyo ef940771d8 .HIDE. Update submodules 2016-11-13 19:06:47 +01:00
Nyo b03d51abff .HIDE. Update submodules 2016-11-13 18:47:10 +01:00
Nyo 8ef02faf36 .HIDE. Update submodules 2016-11-13 12:36:10 +01:00
Nyo d29dcd25f7 .BANCHO. Remove schiavo logs for /np in #spect_* 2016-11-13 12:35:15 +01:00
Nyo cd75d1ad8d .BANCHO. Add bancho components RAM usage as datadog stats 2016-11-13 12:25:38 +01:00
Nyo c7c5528588 .BANCHO. .FIX. Fix /away command, add support for /away command on IRC 2016-11-13 12:23:45 +01:00
Nyo 3b150d70cd .BANCHO. Decrease IRC polling time to 1 second 2016-11-12 22:55:43 +01:00
Nyo d249dd593f .HIDE. Update requirements.txt 2016-11-01 09:57:43 +01:00
Nyo 78a6931805 .HIDE. Update submodules 2016-10-31 16:50:34 +01:00
Nyo 555c9cca1f .HIDE. Update requirements.txt 2016-10-31 16:40:54 +01:00
Nyo 83c514b75e .HIDE. Update submodules 2016-10-31 16:39:54 +01:00
85 changed files with 3135 additions and 1516 deletions

8
.gitignore vendored
View File

@ -1,8 +1,10 @@
**/__pycache__
**/build
config.ini
filters.txt
.data
.idea
common_funzia
common_refractor
common_memato
redistest.py
*.c
*.so
.pyenv

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "common"]
path = common
url = git@git.zxq.co:ripple/ripple-python-common.git
url = https://github.com/osufx/ripple-python-common.git

7
.landscape.yaml Normal file
View File

@ -0,0 +1,7 @@
python-targets:
- 3
pep8:
none: true
pylint:
disable:
- cyclic-import

View File

@ -1,4 +1,8 @@
## pep.py
## pep.py [![Code Health](https://landscape.io/github/osuripple/pep.py/master/landscape.svg?style=flat)](https://landscape.io/github/osuripple/pep.py/master)
- Origin: https://git.zxq.co/ripple/pep.py
- Mirror: https://github.com/osuripple/pep.py
This is Ripple's bancho server. It handles:
- Client login
- Online users listing and statuses
@ -9,6 +13,8 @@ This is Ripple's bancho server. It handles:
## Requirements
- Python 3.5
- Cython
- C compiler
- MySQLdb (`mysqlclient`)
- Tornado
- Bcrypt
@ -23,9 +29,14 @@ afterwards, install the required dependencies with pip
```
$ pip install -r requirements.txt
```
then, run pep.py once to create the default config file and edit it
then, compile all `*.pyx` files to `*.so` or `*.dll` files using `setup.py` (distutils file)
```
$ python3 setup.py build_ext --inplace
```
finally, run pep.py once to create the default config file and edit it
```
$ python3 pep.py
...
$ nano config.ini
```
you can run pep.py by typing

2
common

@ -1 +1 @@
Subproject commit 97ed79d4a1a721bec3ebfe05f11ffde589f97bd2
Subproject commit 6103fe96a79cd8f5cbabe24b5fac9cf2a5cacb4a

View File

@ -1,4 +1,3 @@
""" Contains functions used to read specific client packets from byte stream """
from constants import dataTypes
from helpers import packetHelper
from constants import slotStatuses
@ -100,7 +99,7 @@ def matchSettings(stream):
start += 2
for i in range(0,16):
s = data[0]["slot{}Status".format(str(i))]
if s != slotStatuses.free and s != slotStatuses.locked:
if s != slotStatuses.FREE and s != slotStatuses.LOCKED:
start += 4
# Other settings
@ -144,6 +143,26 @@ def transferHost(stream):
def matchInvite(stream):
return packetHelper.readPacketData(stream, [["userID", dataTypes.UINT32]])
def matchFrames(stream):
return packetHelper.readPacketData(stream,
[
["time", dataTypes.SINT32],
["id", dataTypes.BYTE],
["count300", dataTypes.UINT16],
["count100", dataTypes.UINT16],
["count50", dataTypes.UINT16],
["countGeki", dataTypes.UINT16],
["countKatu", dataTypes.UINT16],
["countMiss", dataTypes.UINT16],
["totalScore", dataTypes.SINT32],
["maxCombo", dataTypes.UINT16],
["currentCombo", dataTypes.UINT16],
["perfect", dataTypes.BYTE],
["currentHp", dataTypes.BYTE],
["tagByte", dataTypes.BYTE],
["usingScoreV2", dataTypes.BYTE]
])
def tournamentMatchInfoRequest(stream):
return packetHelper.readPacketData(stream, [["matchID", dataTypes.UINT32]])

View File

@ -1,5 +1,3 @@
"""Bancho exceptions"""
# TODO: Prints in exceptions
class loginFailedException(Exception):
pass
@ -91,4 +89,22 @@ class unknownStreamException(Exception):
pass
class userTournamentException(Exception):
pass
class userAlreadyInChannelException(Exception):
pass
class userNotInChannelException(Exception):
pass
class missingReportInfoException(Exception):
pass
class invalidUserException(Exception):
pass
class wrongChannelException(Exception):
pass
class periodicLoopException(Exception):
pass

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,2 @@
normal = 0
freeMod = 1
NORMAL = 0
FREE_MOD = 1

View File

@ -1,3 +1,4 @@
score = 0
accuracy = 1
combo = 2
SCORE = 0
ACCURACY = 1
COMBO = 2
SCORE_V2 = 3

View File

@ -1,4 +1,4 @@
headToHead = 0
tagCoop = 1
teamVs = 2
tagTeamVs = 3
HEAD_TO_HEAD = 0
TAG_COOP = 1
TEAM_VS = 2
TAG_TEAM_VS = 3

View File

@ -1,3 +1,3 @@
noTeam = 0
blue = 1
red = 2
NO_TEAM = 0
BLUE = 1
RED = 2

View File

@ -78,5 +78,7 @@ server_userSilenced = 94
server_userPresenceBundle = 96
client_userPanelRequest = 97
client_tournamentMatchInfoRequest = 93
server_matchAbort = 106
server_switchServer = 107
client_tournamentJoinMatchChannel = 108
client_tournamentLeaveMatchChannel = 109

View File

@ -16,12 +16,12 @@ def forceUpdate():
def loginBanned():
packets = packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.SINT32]])
packets += notification("You are banned. You can appeal after one month since your ban by sending an email to support@ripple.moe from the email address you've used to sign up.")
packets += notification("You are banned. You can appeal after one month since your ban by sending an email to {} from the email address you've used to sign up.".format(glob.conf.extra["pep.py"]["support-email"]))
return packets
def loginLocked():
packets = packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.SINT32]])
packets += notification("Your account is locked. You can't log in, but your profile and scores are still visible from the website. If you want to unlock your account, send an email to support@ripple.moe from the email address you've used to sign up.")
packets += notification("Your account is locked. You can't log in, but your profile and scores are still visible from the website. If you want to unlock your account, send an email to {} from the email address you've used to sign up.".format(glob.conf.extra["pep.py"]["support-email"]))
return packets
def loginError():
@ -94,12 +94,12 @@ def userPanel(userID, force = False):
# Get username color according to rank
# Only admins and normal users are currently supported
userRank = 0
if username == "FokaBot":
userRank |= userRanks.MOD
elif userUtils.isInPrivilegeGroup(userID, "community manager"):
if username == glob.BOT_NAME:
userRank |= userRanks.MOD
elif userUtils.isInPrivilegeGroup(userID, "developer"):
userRank |= userRanks.ADMIN
elif userUtils.isInPrivilegeGroup(userID, "chat mod"):
userRank |= userRanks.MOD
elif (userToken.privileges & privileges.USER_DONOR) > 0:
userRank |= userRanks.SUPPORTER
else:
@ -155,11 +155,13 @@ def channelJoinSuccess(userID, chan):
return packetHelper.buildPacket(packetIDs.server_channelJoinSuccess, [[chan, dataTypes.STRING]])
def channelInfo(chan):
if chan not in glob.channels.channels:
return bytes()
channel = glob.channels.channels[chan]
return packetHelper.buildPacket(packetIDs.server_channelInfo, [
[chan, dataTypes.STRING],
[channel.name, dataTypes.STRING],
[channel.description, dataTypes.STRING],
[channel.getConnectedUsersCount(), dataTypes.UINT16]
[len(glob.streams.streams["chat/{}".format(chan)].clients), dataTypes.UINT16]
])
def channelInfoEnd():
@ -200,17 +202,18 @@ def createMatch(matchID):
# Get match binary data and build packet
match = glob.matches.matches[matchID]
return packetHelper.buildPacket(packetIDs.server_newMatch, match.getMatchData())
matchData = match.getMatchData(censored=True)
return packetHelper.buildPacket(packetIDs.server_newMatch, matchData)
# TODO: Add match object argument to save some CPU
def updateMatch(matchID):
def updateMatch(matchID, censored = False):
# Make sure the match exists
if matchID not in glob.matches.matches:
return bytes()
# Get match binary data and build packet
match = glob.matches.matches[matchID]
return packetHelper.buildPacket(packetIDs.server_updateMatch, match.getMatchData())
return packetHelper.buildPacket(packetIDs.server_updateMatch, match.getMatchData(censored=censored))
def matchStart(matchID):
# Make sure the match exists
@ -261,10 +264,18 @@ def playerFailed(slotID):
def matchTransferHost():
return packetHelper.buildPacket(packetIDs.server_matchTransferHost)
def matchAbort():
return packetHelper.buildPacket(packetIDs.server_matchAbort)
def switchServer(address):
return packetHelper.buildPacket(packetIDs.server_switchServer, [[address, dataTypes.STRING]])
""" Other packets """
def notification(message):
return packetHelper.buildPacket(packetIDs.server_notification, [[message, dataTypes.STRING]])
def banchoRestart(msUntilReconnection):
return packetHelper.buildPacket(packetIDs.server_restart, [[msUntilReconnection, dataTypes.UINT32]])
return packetHelper.buildPacket(packetIDs.server_restart, [[msUntilReconnection, dataTypes.UINT32]])
def rtx(message):
return packetHelper.buildPacket(0x69, [[message, dataTypes.STRING]])

View File

@ -1,8 +1,8 @@
free = 1
locked = 2
notReady = 4
ready = 8
noMap = 16
playing = 32
occupied = 124
playingQuit = 128
FREE = 1
LOCKED = 2
NOT_READY = 4
READY = 8
NO_MAP = 16
PLAYING = 32
OCCUPIED = 124
PLAYING_QUIT = 128

View File

@ -1,4 +1,3 @@
"""Bancho user ranks"""
NORMAL = 0
PLAYER = 1
BAT = 2

View File

@ -1,25 +1,21 @@
from common.constants import actions
from common.log import logUtils as log
from common.ripple import userUtils
from constants import clientPackets
from constants import serverPackets
from objects import glob
def handle(userToken, packetData):
# Get usertoken data
userID = userToken.userID
username = userToken.username
# Make sure we are not banned
if userUtils.isBanned(userID):
userToken.enqueue(serverPackets.loginBanned())
return
#if userUtils.isBanned(userID):
# userToken.enqueue(serverPackets.loginBanned())
# return
# Send restricted message if needed
if not userToken.restricted:
if userUtils.isRestricted(userID):
userToken.setRestricted()
#if userToken.restricted:
# userToken.checkRestricted(True)
# Change action packet
packetData = clientPackets.userActionChange(packetData)
@ -35,8 +31,10 @@ if userToken.matchID != -1 and userToken.actionID != actions.MULTIPLAYING and us
'''
# Update cached stats if our pp changed if we've just submitted a score or we've changed gameMode
if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]):
# Always update game mode, or we'll cache stats from the wrong game mode if we've changed it
#if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]):
# Update cached stats if we've changed gamemode
if userToken.gameMode != packetData["gameMode"]:
userToken.gameMode = packetData["gameMode"]
userToken.updateCachedStats()

View File

@ -15,29 +15,29 @@ def handle(userToken, packetData):
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.changeMods(mods.DOUBLETIME)
# Nightcore
if (packetData["mods"] & mods.NIGHTCORE) > 0:
match.changeMods(match.mods + mods.NIGHTCORE)
elif (packetData["mods"] & mods.HALFTIME) > 0:
match.changeMods(mods.HALFTIME)
else:
# No DT/HT, set global mods to 0 (we are in freemod mode)
match.changeMods(0)
with glob.matches.matches[matchID] as match:
if match.matchModMode == matchModModes.FREE_MOD:
# 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.changeMods(mods.DOUBLETIME)
# Nightcore
if (packetData["mods"] & mods.NIGHTCORE) > 0:
match.changeMods(match.mods + mods.NIGHTCORE)
elif (packetData["mods"] & mods.HALFTIME) > 0:
match.changeMods(mods.HALFTIME)
else:
# No DT/HT, set global mods to 0 (we are in freemod mode)
match.changeMods(0)
# Set slot mods
slotID = match.getUserSlotID(userID)
if slotID is not None:
match.setSlotMods(slotID, packetData["mods"])
else:
# Not freemod, set match mods
match.changeMods(packetData["mods"])
# Set slot mods
slotID = match.getUserSlotID(userID)
if slotID is not None:
match.setSlotMods(slotID, packetData["mods"])
else:
# Not freemod, set match mods
match.changeMods(packetData["mods"])

View File

@ -10,12 +10,10 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches:
return
# Get our match
match = glob.matches.matches[matchID]
with glob.matches.matches[matchID] as match:
# Host check
if userToken.userID != match.hostUserID:
return
# Host check
if userToken.userID != match.hostUserID:
return
# Update match password
match.changePassword(packetData["matchPassword"])
# Update match password
match.changePassword(packetData["matchPassword"])

View File

@ -21,97 +21,84 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches:
return
# Get match object
match = glob.matches.matches[matchID]
# Host check
if userToken.userID != match.hostUserID:
return
with glob.matches.matches[matchID] as match:
if userToken.userID != match.hostUserID:
return
# 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"
]
# 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)
# Set match name
match.matchName = packetData["matchName"] if packetData["matchName"] != "meme" else random.choice(memeTitles)
# Update match settings
match.inProgress = packetData["inProgress"]
if packetData["matchPassword"] != "":
match.matchPassword = generalUtils.stringMd5(packetData["matchPassword"])
else:
match.matchPassword = ""
match.beatmapName = packetData["beatmapName"]
match.beatmapID = packetData["beatmapID"]
match.hostUserID = packetData["hostUserID"]
match.gameMode = packetData["gameMode"]
# Update match settings
match.inProgress = packetData["inProgress"]
if packetData["matchPassword"] != "":
match.matchPassword = generalUtils.stringMd5(packetData["matchPassword"])
else:
match.matchPassword = ""
match.beatmapName = packetData["beatmapName"]
match.beatmapID = packetData["beatmapID"]
match.hostUserID = packetData["hostUserID"]
match.gameMode = packetData["gameMode"]
oldBeatmapMD5 = match.beatmapMD5
oldMods = match.mods
oldBeatmapMD5 = match.beatmapMD5
oldMods = match.mods
oldMatchTeamType = match.matchTeamType
match.mods = packetData["mods"]
match.beatmapMD5 = packetData["beatmapMD5"]
match.matchScoringType = packetData["scoringType"]
match.matchTeamType = packetData["teamType"]
match.matchModMode = packetData["freeMods"]
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 ready if needed
if oldMods != match.mods or oldBeatmapMD5 != match.beatmapMD5:
match.resetReady()
# 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
# Reset mods if needed
if match.matchModMode == matchModModes.NORMAL:
# Reset slot mods if not freeMods
match.resetMods()
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
# Initialize teams if team type changed
if match.matchTeamType != oldMatchTeamType:
match.initializeTeams()
# Force no freemods if tag coop
if match.matchTeamType == matchTeamTypes.tagCoop or match.matchTeamType == matchTeamTypes.tagTeamVs:
match.matchModMode = matchModModes.normal
# Force no freemods if tag coop
if match.matchTeamType == matchTeamTypes.TAG_COOP or match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
match.matchModMode = matchModModes.NORMAL
# Send updated settings
match.sendUpdates()
# Send updated settings
match.sendUpdates()
# Console output
log.info("MPROOM{}: Updated room settings".format(match.matchID))
# Console output
log.info("MPROOM{}: Updated room settings".format(match.matchID))

View File

@ -8,8 +8,6 @@ def handle(userToken, packetData):
# Read packet data
packetData = clientPackets.changeSlot(packetData)
# Get match
match = glob.matches.matches[userToken.matchID]
# Change slot
match.userChangeSlot(userID, packetData["slotID"])
with glob.matches.matches[userToken.matchID] as match:
# Change slot
match.userChangeSlot(userID, packetData["slotID"])

View File

@ -1,7 +1,6 @@
from common.log import logUtils as log
from constants import clientPackets
from constants import clientPackets, serverPackets
from constants import exceptions
from constants import serverPackets
from objects import glob
@ -13,26 +12,32 @@ def handle(userToken, packetData):
# Read packet data
packetData = clientPackets.createMatch(packetData)
# Make sure the name is valid
matchName = packetData["matchName"].strip()
if not matchName:
raise exceptions.matchCreateError()
# 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)
# TODO: Player number check (Dirty hack below)
matchID = glob.matches.createMatch(matchName, packetData["matchPassword"].strip(), 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
raise exceptions.matchCreateError()
# Get match object
match = glob.matches.matches[matchID]
with glob.matches.matches[matchID] as match:
# Join that match
userToken.joinMatch(matchID)
# Join that match
userToken.joinMatch(matchID)
# Disable slots (Dirty)
for i in range(0,16):
if match.slots[i].status is not 4:
match.slots[i].status = packetData["slot{}Status".format(i)]
# Give host to match creator
match.setHost(userID)
match.sendUpdates()
match.changePassword(packetData["matchPassword"])
# Console output
log.info("MPROOM{}: Room created!".format(matchID))
# Give host to match creator
match.setHost(userID)
match.sendUpdates()
match.changePassword(packetData["matchPassword"])
except exceptions.matchCreateError:
log.error("Error while creating match!")
userToken.enqueue(serverPackets.matchJoinFail())

View File

@ -1,4 +1,3 @@
from common import generalUtils
from common.log import logUtils as log
from constants import clientPackets
from constants import exceptions
@ -18,21 +17,17 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches:
return
# Match exists, get object
match = glob.matches.matches[matchID]
# Hash password if needed
#if password != "":
# if password != "":
# password = generalUtils.stringMd5(password)
# Check password
# TODO: Admins can enter every match
if match.matchPassword != "":
if match.matchPassword != password:
raise exceptions.matchWrongPasswordException
with glob.matches.matches[matchID] as match:
if match.matchPassword != "" and match.matchPassword != password:
raise exceptions.matchWrongPasswordException()
# Password is correct, join match
userToken.joinMatch(matchID)
# Password is correct, join match
userToken.joinMatch(matchID)
except exceptions.matchWrongPasswordException:
userToken.enqueue(serverPackets.matchJoinFail())
log.warning("{} has tried to join a mp room, but he typed the wrong password".format(userToken.username))

View File

@ -15,6 +15,7 @@ from objects import glob
def handle(tornadoRequest):
# Data to return
responseToken = None
responseTokenString = "ayy"
responseData = bytes()
@ -29,9 +30,6 @@ def handle(tornadoRequest):
# 2:-3 thing is because requestData has some escape stuff that we don't need
loginData = str(tornadoRequest.request.body)[2:-3].split("\\n")
try:
# If true, print error to console
err = False
# Make sure loginData is valid
if len(loginData) < 3:
raise exceptions.invalidArgumentsException()
@ -46,7 +44,6 @@ def handle(tornadoRequest):
splitData = loginData[2].split("|")
osuVersion = splitData[0]
timeOffset = int(splitData[1])
print(str(timeOffset))
clientData = splitData[3].split(":")[:5]
if len(clientData) < 4:
raise exceptions.forceUpdateException()
@ -64,9 +61,9 @@ def handle(tornadoRequest):
# Make sure we are not banned or locked
priv = userUtils.getPrivileges(userID)
if userUtils.isBanned(userID) == True and priv & privileges.USER_PENDING_VERIFICATION == 0:
if userUtils.isBanned(userID) and priv & privileges.USER_PENDING_VERIFICATION == 0:
raise exceptions.loginBannedException()
if userUtils.isLocked(userID) == True and priv & privileges.USER_PENDING_VERIFICATION == 0:
if userUtils.isLocked(userID) and priv & privileges.USER_PENDING_VERIFICATION == 0:
raise exceptions.loginLockedException()
# 2FA check
@ -78,7 +75,7 @@ def handle(tornadoRequest):
# Verify this user (if pending activation)
firstLogin = False
if priv & privileges.USER_PENDING_VERIFICATION > 0 or userUtils.hasVerifiedHardware(userID) == False:
if priv & privileges.USER_PENDING_VERIFICATION > 0 or not userUtils.hasVerifiedHardware(userID):
if userUtils.verifyUser(userID, clientData):
# Valid account
log.info("Account {} verified successfully!".format(userID))
@ -121,6 +118,9 @@ def handle(tornadoRequest):
expireIn = "{} days".format(expireDays) if expireDays > 1 else "less than 24 hours"
responseToken.enqueue(serverPackets.notification("Your donor tag expires in {}! When your donor tag expires, you won't have any of the donor privileges, like yellow username, custom badge and discord custom role and username color! If you wish to keep supporting Ripple and you don't want to lose your donor privileges, you can donate again by clicking on 'Support us' on Ripple's website.".format(expireIn)))
# Deprecate telegram 2fa and send alert
if userUtils.deprecateTelegram2Fa(userID):
responseToken.enqueue(serverPackets.notification("As stated on our blog, Telegram 2FA has been deprecated on 29th June 2018. Telegram 2FA has just been disabled from your account. If you want to keep your account secure with 2FA, please enable TOTP-based 2FA from our website https://ripple.moe. Thank you for your patience."))
# Set silence end UNIX time in token
responseToken.silenceEndTime = userUtils.getSilenceEnd(userID)
@ -176,7 +176,7 @@ def handle(tornadoRequest):
# Output channels info
for key, value in glob.channels.channels.items():
if value.publicRead == True and value.hidden == False:
if value.publicRead and not value.hidden:
responseToken.enqueue(serverPackets.channelInfo(key))
# Send friends list
@ -186,25 +186,29 @@ def handle(tornadoRequest):
if glob.banchoConf.config["menuIcon"] != "":
responseToken.enqueue(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
# Send online users IDs array
responseToken.enqueue(serverPackets.onlineUsers())
# Send online users' panels
with glob.tokens:
for _, token in glob.tokens.tokens.items():
if not token.restricted:
responseToken.enqueue(serverPackets.userPanel(token.userID))
# Get location and country from ip.zxq.co or database
if glob.localize:
# Get location and country from IP
location = locationHelper.getLocation(requestIP)
latitude, longitude = locationHelper.getLocation(requestIP)
countryLetters = locationHelper.getCountry(requestIP)
country = countryHelper.getCountryID(countryLetters)
else:
# Set location to 0,0 and get country from db
log.warning("Location skipped")
location = [0,0]
latitude = 0
longitude = 0
countryLetters = "XX"
country = countryHelper.getCountryID(userUtils.getCountry(userID))
# Set location and country
responseToken.setLocation(location)
responseToken.setCountry(country)
responseToken.setLocation(latitude, longitude)
responseToken.country = country
# Set country in db if user has no country (first bancho login)
if userUtils.getCountry(userID) == "XX":
@ -220,25 +224,23 @@ def handle(tornadoRequest):
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.invalidArgumentsException:
# Invalid POST data
# (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.loginFailed()
responseData += serverPackets.notification("I see what you're doing...")
except exceptions.loginBannedException:
# Login banned error packet
err = True
responseData += serverPackets.loginBanned()
except exceptions.loginLockedException:
# Login banned error packet
err = True
responseData += serverPackets.loginLocked()
except exceptions.banchoMaintenanceException:
# Bancho is in maintenance mode
responseData = responseToken.queue
responseData = bytes()
if responseToken is not None:
responseData = responseToken.queue
responseData += serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.")
responseData += serverPackets.loginFailed()
except exceptions.banchoRestartingException:
@ -251,7 +253,6 @@ def handle(tornadoRequest):
except exceptions.haxException:
# Using oldoldold client, we don't have client data. Force update.
# (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.forceUpdate()
responseData += serverPackets.notification("Hory shitto, your client is TOO old! Nice prehistory! Please turn update it from the settings!")
except:
@ -259,10 +260,7 @@ def handle(tornadoRequest):
finally:
# Console and discord log
if len(loginData) < 3:
msg = "Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP)
else:
msg = "Bancho login request from **{}** for user **{}** ({})".format(requestIP, loginData[0], "failed" if err == True else "success")
log.info(msg, "bunker")
log.info("Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP), "bunker")
# Return token string and data
return responseTokenString, responseData
return responseTokenString, responseData

View File

@ -1,4 +1,5 @@
import time
import json
from common.log import logUtils as log
from constants import serverPackets
@ -6,7 +7,7 @@ from helpers import chatHelper as chat
from objects import glob
def handle(userToken, _=None):
def handle(userToken, _=None, deleteToken=True):
# get usertoken data
userID = userToken.userID
username = userToken.username
@ -16,7 +17,7 @@ def handle(userToken, _=None):
# 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 or userToken.irc):
if int(time.time() - userToken.loginTime) >= 5 or userToken.irc:
# Stop spectating
userToken.stopSpectating()
@ -34,11 +35,23 @@ def handle(userToken, _=None):
glob.streams.broadcast("main", serverPackets.userLogout(userID))
# Disconnect from IRC if needed
if userToken.irc == True and glob.irc == True:
if userToken.irc and glob.irc:
glob.ircServer.forceDisconnection(userToken.username)
# Delete token
glob.tokens.deleteToken(requestToken)
if deleteToken:
glob.tokens.deleteToken(requestToken)
else:
userToken.kicked = True
# Change username if needed
newUsername = glob.redis.get("ripple:change_username_pending:{}".format(userID))
if newUsername is not None:
log.debug("Sending username change request for user {}".format(userID))
glob.redis.publish("peppy:change_username", json.dumps({
"userID": userID,
"newUsername": newUsername.decode("utf-8")
}))
# Console output
log.info("{} has been disconnected. (logout)".format(username))

View File

@ -1,6 +1,6 @@
from objects import glob
def handle(userToken, packetData, has):
def handle(userToken, _, has):
# Get usertoken data
userID = userToken.userID
@ -15,8 +15,6 @@ def handle(userToken, packetData, has):
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)
with glob.matches.matches[matchID] as match:
match.userHasBeatmap(userID, has)

View File

@ -15,8 +15,6 @@ def handle(userToken, _):
if matchID not in glob.matches.matches:
return
# Get match object
match = glob.matches.matches[matchID]
# Change team
match.changeTeam(userID)
with glob.matches.matches[matchID] as match:
match.changeTeam(userID)

View File

@ -1,6 +1,6 @@
from objects import glob
def handle(userToken, packetData):
def handle(userToken, _):
# Get usertoken data
userID = userToken.userID
@ -15,8 +15,6 @@ def handle(userToken, packetData):
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)
with glob.matches.matches[matchID] as match:
match.playerCompleted(userID)

View File

@ -15,8 +15,6 @@ def handle(userToken, _):
if matchID not in glob.matches.matches:
return
# Match exists, get object
match = glob.matches.matches[matchID]
# Fail user
match.playerFailed(userID)
with glob.matches.matches[matchID] as match:
match.playerFailed(userID)

View File

@ -1,6 +1,5 @@
from objects import glob
from constants import slotStatuses
from constants import serverPackets
from constants import serverPackets, clientPackets
def handle(userToken, packetData):
# Get usertoken data
@ -17,11 +16,16 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches:
return
# The match exists, get object
match = glob.matches.matches[matchID]
# Parse the data
data = clientPackets.matchFrames(packetData)
# Change slot id in packetData
slotID = match.getUserSlotID(userID)
with glob.matches.matches[matchID] as match:
# Change slot id in packetData
slotID = match.getUserSlotID(userID)
# Enqueue frames to who's playing
glob.streams.broadcast(match.playingStreamName, serverPackets.matchFrames(slotID, packetData))
# Update the score
match.updateScore(slotID, data["totalScore"])
match.updateHP(slotID, data["currentHp"])
# Enqueue frames to who's playing
glob.streams.broadcast(match.playingStreamName, serverPackets.matchFrames(slotID, packetData))

View File

@ -17,8 +17,6 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches:
return
# Get match object
match = glob.matches.matches[matchID]
# Send invite
match.invite(userID, packetData["userID"])
with glob.matches.matches[matchID] as match:
match.invite(userID, packetData["userID"])

View File

@ -12,16 +12,16 @@ def handle(userToken, packetData):
matchID = userToken.matchID
if matchID not in glob.matches.matches:
return
match = glob.matches.matches[matchID]
# Host check
if userID != match.hostUserID:
return
with glob.matches.matches[matchID] as match:
# Host check
if userID != match.hostUserID:
return
# Make sure we aren't locking our slot
ourSlot = match.getUserSlotID(userID)
if packetData["slotID"] == ourSlot:
return
# Make sure we aren't locking our slot
ourSlot = match.getUserSlotID(userID)
if packetData["slotID"] == ourSlot:
return
# Lock/Unlock slot
match.toggleSlotLock(packetData["slotID"])
# Lock/Unlock slot
match.toggleSlotLocked(packetData["slotID"])

View File

@ -1,6 +1,6 @@
from objects import glob
def handle(userToken, packetData):
def handle(userToken, _):
# Get userToken data
userID = userToken.userID
@ -15,8 +15,6 @@ def handle(userToken, packetData):
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)
with glob.matches.matches[matchID] as match:
match.playerLoaded(userID)

View File

@ -8,9 +8,14 @@ def handle(userToken, _):
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 is not None:
match.toggleSlotReady(slotID)
with glob.matches.matches[matchID] as match:
# Get our slotID and change ready status
slotID = match.getUserSlotID(userID)
if slotID is not None:
match.toggleSlotReady(slotID)
# If this is a tournament match, we should send the current status of ready
# players.
if match.isTourney:
match.sendReadyStatus()

View File

@ -1,6 +1,6 @@
from objects import glob
def handle(userToken, packetData):
def handle(userToken, _):
# Get userToken data
userID = userToken.userID
@ -15,8 +15,6 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches:
return
# The match exists, get object
match = glob.matches.matches[matchID]
# Skip
match.playerSkip(userID)
with glob.matches.matches[matchID] as match:
match.playerSkip(userID)

View File

@ -1,6 +1,4 @@
from objects import glob
from constants import slotStatuses
from constants import serverPackets
def handle(userToken, _):
@ -15,11 +13,9 @@ def handle(userToken, _):
if matchID not in glob.matches.matches:
return
# The match exists, get object
match = glob.matches.matches[matchID]
with glob.matches.matches[matchID] as match:
# Host check
if userToken.userID != match.hostUserID:
return
# Host check
if userToken.userID != match.hostUserID:
return
match.start()
match.start()

View File

@ -16,12 +16,10 @@ def handle(userToken, packetData):
if matchID not in glob.matches.matches:
return
# Match exists, get object
match = glob.matches.matches[matchID]
# Host check
if userToken.userID != match.hostUserID:
return
with glob.matches.matches[matchID] as match:
if userToken.userID != match.hostUserID:
return
# Transfer host
match.transferHost(packetData["slotID"])
# Transfer host
match.transferHost(packetData["slotID"])

View File

@ -1,17 +1,15 @@
from common.log import logUtils as log
from helpers import chatHelper as chat
from objects import glob
def handle(userToken, _):
# Get usertoken data
userID = userToken.userID
username = userToken.username
# Remove user from users in lobby
userToken.leaveStream("lobby")
# Part lobby channel
# Done automatically by the client
chat.partChannel(channel="#lobby", token=userToken, kick=True)
# Console output

View File

@ -1,6 +1,7 @@
from common.log import logUtils as log
from constants import clientPackets
from constants import serverPackets
from objects import glob
def handle(userToken, packetData):
@ -11,12 +12,12 @@ def handle(userToken, packetData):
packetData = clientPackets.setAwayMessage(packetData)
# Set token away message
userToken.setAwayMessage(packetData["awayMessage"])
userToken.awayMessage = 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))
userToken.enqueue(serverPackets.sendMessage(glob.BOT_NAME, username, fokaMessage))
log.info("{} has changed their away message to: {}".format(username, packetData["awayMessage"]))

View File

@ -1,31 +1,15 @@
from objects import glob
from constants import serverPackets
from constants import exceptions
from common.log import logUtils as log
def handle(userToken, packetData):
# get token data
userID = userToken.userID
# Send spectator frames to every spectator
glob.streams.broadcast("spect/{}".format(userID), serverPackets.spectatorFrames(packetData[7:]))
'''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 is 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))'''
streamName = "spect/{}".format(userID)
glob.streams.broadcast(streamName, serverPackets.spectatorFrames(packetData[7:]))
log.debug("Broadcasting {}'s frames to {} clients".format(
userID,
len(glob.streams.streams[streamName].clients))
)

View File

@ -8,6 +8,11 @@ def handle(userToken, packetData):
# Start spectating packet
packetData = clientPackets.startSpectating(packetData)
# If the user id is less than 0, treat this as a stop spectating packet
if packetData["userID"] < 0:
userToken.stopSpectating()
return
# Get host token
targetToken = glob.tokens.getTokenFromUserID(packetData["userID"])
if targetToken is None:

View File

@ -5,7 +5,7 @@ from helpers import chatHelper as chat
def handle(userToken, packetData):
packetData = clientPackets.tournamentJoinMatchChannel(packetData)
matchID = packetData["matchID"]
if matchID not in glob.matches.matches:
if matchID not in glob.matches.matches or not userToken.tournament:
return
userToken.matchID = matchID
chat.joinChannel(token=userToken, channel="#multi_{}".format(matchID))
chat.joinChannel(token=userToken, channel="#multi_{}".format(matchID), force=True)

View File

@ -5,7 +5,7 @@ from helpers import chatHelper as chat
def handle(userToken, packetData):
packetData = clientPackets.tournamentLeaveMatchChannel(packetData)
matchID = packetData["matchID"]
if matchID not in glob.matches.matches:
if matchID not in glob.matches.matches or not userToken.tournament:
return
chat.partChannel(token=userToken, channel="#multi_{}".format(matchID))
chat.partChannel(token=userToken, channel="#multi_{}".format(matchID), force=True)
userToken.matchID = 0

View File

@ -1,10 +1,10 @@
from constants import clientPackets
from constants import serverPackets
from objects import glob
def handle(userToken, packetData):
packetData = clientPackets.tournamentMatchInfoRequest(packetData)
matchID = packetData["matchID"]
if matchID not in glob.matches.matches:
if matchID not in glob.matches.matches or not userToken.tournament:
return
userToken.enqueue(glob.matches.matches[matchID].matchDataCache)
with glob.matches.matches[matchID] as m:
userToken.enqueue(m.matchDataCache)

View File

@ -1,16 +1,23 @@
fuck=firetruck
shit=shish
ass=peach
asses=peaches
bitch=fine lady
bitches=fine ladies
asshole=donkey
ass hole=donkey
cock=chicken
cocks=chickens
dick=eggplant
dicks=eggplants
boobs=bob
tits=teeth
cum=yogurt
cunt=count
fuck=firetruck
shit=shish
ass=peach
asses=peaches
bitch=fine lady
bitches=fine ladies
asshole=donkey
ass hole=donkey
cock=chicken
cocks=chickens
dick=eggplant
dicks=eggplants
boobs=bob
tits=teeth
cum=yogurt
cunt=count
nigger=flowers
ngger=flowers
niggers=flowers
weed=grass
AQN=meme
theaquila=meme
aquila=meme

4
full_build.sh Normal file
View File

@ -0,0 +1,4 @@
find . -name "*.c" -type f -delete
find . -name "*.o" -type f -delete
find . -name "*.so" -type f -delete
python3 setup.py build_ext --inplace

View File

@ -1,6 +1,9 @@
import json
from common.log import logUtils as log
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager
from constants import exceptions
from helpers import chatHelper
@ -8,6 +11,9 @@ from objects import glob
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self):
statusCode = 400
data = {"message": "unknown error"}
@ -21,8 +27,11 @@ class handler(requestsManager.asyncRequestHandler):
if key is None or key != glob.conf.config["server"]["cikey"]:
raise exceptions.invalidArgumentsException()
log.info("API REQUEST FOR FOKABOT MESSAGE AAAAAAA")
chatHelper.sendMessage("FokaBot", self.get_argument("to"), self.get_argument("msg"))
chatHelper.sendMessage(
glob.BOT_NAME,
self.get_argument("to").encode().decode("ASCII", "ignore"),
self.get_argument("msg").encode().decode("ASCII", "ignore")
)
# Status code and message
statusCode = 200
@ -35,7 +44,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode
# Send response
#self.clear()
self.write(json.dumps(data))
self.set_status(statusCode)
#self.finish(json.dumps(data))
self.set_status(statusCode)

View File

@ -1,11 +1,19 @@
import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.ripple import userUtils
from common.web import requestsManager
from constants import exceptions
from objects import glob
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self):
statusCode = 400
data = {"message": "unknown error"}
@ -18,7 +26,8 @@ class handler(requestsManager.asyncRequestHandler):
username = None
userID = None
if "u" in self.request.arguments:
username = self.get_argument("u")
#username = self.get_argument("u").lower().replace(" ", "_")
username = userUtils.safeUsername(self.get_argument("u"))
else:
try:
userID = int(self.get_argument("id"))
@ -29,7 +38,7 @@ class handler(requestsManager.asyncRequestHandler):
data["result"] = False
else:
if username is not None:
data["result"] = True if glob.tokens.getTokenFromUsername(username) is not None else False
data["result"] = True if glob.tokens.getTokenFromUsername(username, safe=True) is not None else False
else:
data["result"] = True if glob.tokens.getTokenFromUserID(userID) is not None else False
@ -44,7 +53,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode
# Send response
#self.clear()
self.write(json.dumps(data))
self.set_status(statusCode)
#self.finish(json.dumps(data))

View File

@ -1,16 +1,23 @@
import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager
from objects import glob
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self):
statusCode = 400
data = {"message": "unknown error"}
try:
# Get online users count
data["result"] = len(glob.tokens.tokens)
data["result"] = int(glob.redis.get("ripple:online_users").decode("utf-8"))
# Status code and message
statusCode = 200
@ -20,7 +27,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode
# Send response
#self.clear()
self.write(json.dumps(data))
self.set_status(statusCode)
#self.finish(json.dumps(data))

View File

@ -1,16 +1,23 @@
import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager
from objects import glob
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self):
statusCode = 400
data = {"message": "unknown error"}
try:
# Get online users count
data["result"] = -1 if glob.restarting == True else 1
data["result"] = -1 if glob.restarting else 1
# Status code and message
statusCode = 200
@ -20,7 +27,5 @@ class handler(requestsManager.asyncRequestHandler):
data["status"] = statusCode
# Send response
#self.clear()
self.write(json.dumps(data))
self.set_status(statusCode)
#self.finish(json.dumps(data))

View File

@ -1,11 +1,18 @@
import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager
from constants import exceptions
from objects import glob
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self):
statusCode = 400
data = {"message": "unknown error"}

View File

@ -1,5 +1,9 @@
import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.log import logUtils as log
from common.web import requestsManager
from constants import exceptions
@ -8,6 +12,9 @@ from objects import glob
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self):
statusCode = 400
data = {"message": "unknown error"}

View File

@ -1,270 +0,0 @@
import datetime
import gzip
import sys
import traceback
import tornado.gen
import tornado.web
from raven.contrib.tornado import SentryMixin
from common.log import logUtils as log
from common.web import requestsManager
from constants import exceptions
from constants import packetIDs
from constants import serverPackets
from events import cantSpectateEvent
from events import changeActionEvent
from events import changeMatchModsEvent
from events import changeMatchPasswordEvent
from events import changeMatchSettingsEvent
from events import changeSlotEvent
from events import channelJoinEvent
from events import channelPartEvent
from events import createMatchEvent
from events import friendAddEvent
from events import friendRemoveEvent
from events import joinLobbyEvent
from events import joinMatchEvent
from events import loginEvent
from events import logoutEvent
from events import matchChangeTeamEvent
from events import matchCompleteEvent
from events import matchFailedEvent
from events import matchFramesEvent
from events import matchHasBeatmapEvent
from events import matchInviteEvent
from events import matchLockEvent
from events import matchNoBeatmapEvent
from events import matchPlayerLoadEvent
from events import matchReadyEvent
from events import matchSkipEvent
from events import matchStartEvent
from events import matchTransferHostEvent
from events import partLobbyEvent
from events import partMatchEvent
from events import requestStatusUpdateEvent
from events import sendPrivateMessageEvent
from events import sendPublicMessageEvent
from events import setAwayMessageEvent
from events import spectateFramesEvent
from events import startSpectatingEvent
from events import stopSpectatingEvent
from events import userPanelRequestEvent
from events import userStatsRequestEvent
from events import tournamentMatchInfoRequestEvent
from events import tournamentJoinMatchChannelEvent
from events import tournamentLeaveMatchChannelEvent
from helpers import packetHelper
from objects import glob
class handler(SentryMixin, requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
def asyncPost(self):
try:
# Track time if needed
if glob.outputRequestTime:
# Start time
st = datetime.datetime.now()
# Client's token string and request data
requestTokenString = self.request.headers.get("osu-token")
requestData = self.request.body
# Server's token string and request data
responseTokenString = "ayy"
responseData = bytes()
if requestTokenString is None:
# No token, first request. Handle login.
responseTokenString, responseData = loginEvent.handle(self)
else:
userToken = None # default value
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 and lock it
userToken = glob.tokens.tokens[requestTokenString]
userToken.lock.acquire()
# 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 glob.outputPackets == True and packetID != 4:
log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData)))
# 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_changeAction: handleEvent(changeActionEvent),
packetIDs.client_logout: handleEvent(logoutEvent),
packetIDs.client_friendAdd: handleEvent(friendAddEvent),
packetIDs.client_friendRemove: handleEvent(friendRemoveEvent),
packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent),
packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent),
packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent),
packetIDs.client_channelJoin: handleEvent(channelJoinEvent),
packetIDs.client_channelPart: handleEvent(channelPartEvent),
packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent),
packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent),
packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent),
packetIDs.client_startSpectating: handleEvent(startSpectatingEvent),
packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent),
packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent),
packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent),
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_matchChangeTeam: handleEvent(matchChangeTeamEvent),
packetIDs.client_invite: handleEvent(matchInviteEvent),
packetIDs.client_tournamentMatchInfoRequest: handleEvent(tournamentMatchInfoRequestEvent),
packetIDs.client_tournamentJoinMatchChannel: handleEvent(tournamentJoinMatchChannelEvent),
packetIDs.client_tournamentLeaveMatchChannel: handleEvent(tournamentLeaveMatchChannelEvent),
}
# Packets processed if in restricted mode.
# All other packets will be ignored if the user is in restricted mode
packetsRestricted = [
packetIDs.client_logout,
packetIDs.client_userStatsRequest,
packetIDs.client_requestStatusUpdate,
packetIDs.client_userPanelRequest,
packetIDs.client_changeAction,
packetIDs.client_channelJoin,
packetIDs.client_channelPart,
]
# Process/ignore packet
if packetID != 4:
if packetID in eventHandler:
if userToken.restricted == False or (userToken.restricted == True and packetID in packetsRestricted):
eventHandler[packetID]()
else:
log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID))
else:
log.warning("Unknown packet id from {} ({})".format(requestTokenString, packetID))
# 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()
except exceptions.tokenNotFoundException:
# Token not found. Disconnect that user
responseData = serverPackets.loginError()
responseData += serverPackets.notification("Whoops! Something went wrong, please login again.")
log.warning("Received packet from unknown token ({}).".format(requestTokenString))
log.info("{} has been disconnected (invalid token)".format(requestTokenString))
finally:
# Unlock token
if userToken is not None:
# Update ping time for timeout
userToken.updatePingTime()
# Release token lock
userToken.lock.release()
if glob.outputRequestTime:
# End time
et = datetime.datetime.now()
# Total time:
tt = float((et.microsecond-st.microsecond)/1000)
log.debug("Request time: {}ms".format(tt))
# Send server's response to client
# We don't use token object because we might not have a token (failed login)
if glob.gzip:
# First, write the gzipped response
self.write(gzip.compress(responseData, int(glob.conf.config["server"]["gziplevel"])))
# Then, add gzip headers
self.add_header("Vary", "Accept-Encoding")
self.add_header("Content-Encoding", "gzip")
else:
# First, write the response
self.write(responseData)
# Add all the headers AFTER the response has been written
self.set_status(200)
self.add_header("cho-token", responseTokenString)
self.add_header("cho-protocol", "19")
self.add_header("Connection", "keep-alive")
self.add_header("Keep-Alive", "timeout=5, max=100")
self.add_header("Content-Type", "text/html; charset=UTF-8")
except:
log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
if glob.sentry:
yield tornado.gen.Task(self.captureException, exc_info=True)
#finally:
# self.finish()
@tornado.web.asynchronous
@tornado.gen.engine
def asyncGet(self):
html = "<html><head><title>MA MAURO ESISTE?</title><style type='text/css'>body{width:30%}</style></head><body><pre>"
html += " _ __<br>"
html += " (_) / /<br>"
html += " ______ __ ____ ____ / /____<br>"
html += " / ___/ / _ \\/ _ \\/ / _ \\<br>"
html += " / / / / /_) / /_) / / ____/<br>"
html += "/__/ /__/ .___/ .___/__/ \\_____/<br>"
html += " / / / /<br>"
html += " /__/ /__/<br>"
html += "<b>PYTHON > ALL VERSION</b><br><br>"
html += "<marquee style='white-space:pre;'><br>"
html += " .. o .<br>"
html += " o.o o . o<br>"
html += " oo...<br>"
html += " __[]__<br>"
html += " phwr--> _\\:D/_/o_o_o_|__ <span style=\"font-family: 'Comic Sans MS'; font-size: 8pt;\">u wot m8</span><br>"
html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/<br>"
html += " \\ . .. .. . /<br>"
html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<br>"
html += "</marquee><br><strike>reverse engineering a protocol impossible to reverse engineer since always</strike><br>we are actually reverse engineering bancho successfully. for the third time.<br><br><i>&copy; Ripple team, 2016</i></pre></body></html>"
self.write(html)

266
handlers/mainHandler.pyx Normal file
View File

@ -0,0 +1,266 @@
import datetime
import gzip
import sys
import traceback
import tornado.gen
import tornado.web
from raven.contrib.tornado import SentryMixin
from common.log import logUtils as log
from common.web import requestsManager
from constants import exceptions
from constants import packetIDs
from constants import serverPackets
from events import cantSpectateEvent
from events import changeActionEvent
from events import changeMatchModsEvent
from events import changeMatchPasswordEvent
from events import changeMatchSettingsEvent
from events import changeSlotEvent
from events import channelJoinEvent
from events import channelPartEvent
from events import createMatchEvent
from events import friendAddEvent
from events import friendRemoveEvent
from events import joinLobbyEvent
from events import joinMatchEvent
from events import loginEvent
from events import logoutEvent
from events import matchChangeTeamEvent
from events import matchCompleteEvent
from events import matchFailedEvent
from events import matchFramesEvent
from events import matchHasBeatmapEvent
from events import matchInviteEvent
from events import matchLockEvent
from events import matchNoBeatmapEvent
from events import matchPlayerLoadEvent
from events import matchReadyEvent
from events import matchSkipEvent
from events import matchStartEvent
from events import matchTransferHostEvent
from events import partLobbyEvent
from events import partMatchEvent
from events import requestStatusUpdateEvent
from events import sendPrivateMessageEvent
from events import sendPublicMessageEvent
from events import setAwayMessageEvent
from events import spectateFramesEvent
from events import startSpectatingEvent
from events import stopSpectatingEvent
from events import userPanelRequestEvent
from events import userStatsRequestEvent
from events import tournamentMatchInfoRequestEvent
from events import tournamentJoinMatchChannelEvent
from events import tournamentLeaveMatchChannelEvent
from helpers import packetHelper
from objects import glob
from common.sentry import sentry
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncPost(self):
# Track time if needed
if glob.outputRequestTime:
# Start time
st = datetime.datetime.now()
# Client's token string and request data
requestTokenString = self.request.headers.get("osu-token")
requestData = self.request.body
# Server's token string and request data
responseTokenString = "ayy"
responseData = bytes()
if requestTokenString is None:
# No token, first request. Handle login.
responseTokenString, responseData = loginEvent.handle(self)
else:
userToken = None # default value
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 and lock it
userToken = glob.tokens.tokens[requestTokenString]
userToken.processingLock.acquire()
# 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 glob.outputPackets and packetID != 4:
log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData)))
# Event handler
def handleEvent(ev):
def wrapper():
ev.handle(userToken, packetData)
return wrapper
eventHandler = {
packetIDs.client_changeAction: handleEvent(changeActionEvent),
packetIDs.client_logout: handleEvent(logoutEvent),
packetIDs.client_friendAdd: handleEvent(friendAddEvent),
packetIDs.client_friendRemove: handleEvent(friendRemoveEvent),
packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent),
packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent),
packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent),
packetIDs.client_channelJoin: handleEvent(channelJoinEvent),
packetIDs.client_channelPart: handleEvent(channelPartEvent),
packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent),
packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent),
packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent),
packetIDs.client_startSpectating: handleEvent(startSpectatingEvent),
packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent),
packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent),
packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent),
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_matchChangeTeam: handleEvent(matchChangeTeamEvent),
packetIDs.client_invite: handleEvent(matchInviteEvent),
packetIDs.client_tournamentMatchInfoRequest: handleEvent(tournamentMatchInfoRequestEvent),
packetIDs.client_tournamentJoinMatchChannel: handleEvent(tournamentJoinMatchChannelEvent),
packetIDs.client_tournamentLeaveMatchChannel: handleEvent(tournamentLeaveMatchChannelEvent),
}
# Packets processed if in restricted mode.
# All other packets will be ignored if the user is in restricted mode
packetsRestricted = [
packetIDs.client_logout,
packetIDs.client_userStatsRequest,
packetIDs.client_requestStatusUpdate,
packetIDs.client_userPanelRequest,
packetIDs.client_changeAction,
packetIDs.client_channelJoin,
packetIDs.client_channelPart,
]
# Process/ignore packet
if packetID != 4:
if packetID in eventHandler:
if not userToken.restricted or (userToken.restricted and packetID in packetsRestricted):
eventHandler[packetID]()
else:
log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID))
else:
log.warning("Unknown packet id from {} ({})".format(requestTokenString, packetID))
# 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()
except exceptions.tokenNotFoundException:
# Token not found. Disconnect that user
responseData = serverPackets.loginError()
responseData += serverPackets.notification("Whoops! Something went wrong, please login again.")
log.warning("Received packet from unknown token ({}).".format(requestTokenString))
log.info("{} has been disconnected (invalid token)".format(requestTokenString))
finally:
# Unlock token
if userToken is not None:
# Update ping time for timeout
userToken.updatePingTime()
# Release processing lock
userToken.processingLock.release()
# Delete token if kicked
if userToken.kicked:
glob.tokens.deleteToken(userToken)
if glob.outputRequestTime:
# End time
et = datetime.datetime.now()
# Total time:
tt = float((et.microsecond-st.microsecond)/1000)
log.debug("Request time: {}ms".format(tt))
# Send server's response to client
# We don't use token object because we might not have a token (failed login)
if glob.gzip:
# First, write the gzipped response
self.write(gzip.compress(responseData, int(glob.conf.config["server"]["gziplevel"])))
# Then, add gzip headers
self.add_header("Vary", "Accept-Encoding")
self.add_header("Content-Encoding", "gzip")
else:
# First, write the response
self.write(responseData)
# Add all the headers AFTER the response has been written
self.set_status(200)
self.add_header("cho-token", responseTokenString)
self.add_header("cho-protocol", "19")
self.add_header("Connection", "keep-alive")
self.add_header("Keep-Alive", "timeout=5, max=100")
self.add_header("Content-Type", "text/html; charset=UTF-8")
@tornado.web.asynchronous
@tornado.gen.engine
def asyncGet(self):
html = "<html><head><title>MA MAURO ESISTE?</title><style type='text/css'>body{width:30%;background:#222;color:#fff;}</style></head><body><pre>"
html += " _ __<br>"
html += " (_) / /<br>"
html += " ______ __ ____ ____ / /____<br>"
html += " / ___/ / _ \\/ _ \\/ / _ \\<br>"
html += " / / / / /_) / /_) / / ____/<br>"
html += "/__/ /__/ .___/ .___/__/ \\_____/<br>"
html += " / / / /<br>"
html += " /__/ /__/<br>"
html += "<b>PYTHON > ALL VERSION</b><br><br>"
html += "<marquee style='white-space:pre;'><br>"
html += " .. o .<br>"
html += " o.o o . o<br>"
html += " oo...<br>"
html += " __[]__<br>"
html += " phwr--> _\\:D/_/o_o_o_|__ <span style=\"font-family: 'Comic Sans MS'; font-size: 8pt;\">u wot m8</span><br>"
html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/<br>"
html += " \\ . .. .. . /<br>"
html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<br>"
html += "</marquee><br><strike>reverse engineering a protocol impossible to reverse engineer since always</strike><br>we are actually reverse engineering bancho successfully. for the third time.<br><br>Running osufx branch.<br><i>&copy; Ripple team, 2016</i></pre></body></html>"
self.write(html)

View File

@ -8,18 +8,16 @@ from objects import fokabot
from objects import glob
def joinChannel(userID = 0, channel = "", token = None, toIRC = True):
def joinChannel(userID = 0, channel = "", token = None, toIRC = True, force=False):
"""
Join a channel
userID -- user ID of the user that joins the channel. Optional.
token can be used instead.
token -- user token object of user that joins the channel. Optional.
userID can be used instead.
channel -- name of channe
toIRC -- if True, send this channel join event to IRC. Must be true if joining from bancho.
Optional. Defaukt: True
return -- returns 0 if joined or other IRC code in case of error. Needed only on IRC-side
:param userID: user ID of the user that joins the channel. Optional. token can be used instead.
:param token: user token object of user that joins the channel. Optional. userID can be used instead.
:param channel: channel name
:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Default: True
:param force: whether to allow game clients to join #spect_ and #multi_ channels
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
"""
try:
# Get token if not defined
@ -30,76 +28,67 @@ def joinChannel(userID = 0, channel = "", token = None, toIRC = True):
raise exceptions.userNotFoundException
else:
token = token
userID = token.userID
# Get usertoken data
username = token.username
# Normal channel, do check stuff
# Make sure the channel exists
if channel not in glob.channels.channels:
raise exceptions.channelUnknownException
raise exceptions.channelUnknownException()
# Check channel permissions
# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually
channelObject = glob.channels.channels[channel]
if channelObject.publicRead == False and token.admin == False:
raise exceptions.channelNoPermissionsException
# Add our userID to users in that channel
channelObject.userJoin(userID)
if channelObject.isSpecial and not token.irc and not force:
raise exceptions.channelUnknownException()
# Add the channel to our joined channel
token.joinChannel(channel)
# Send channel joined (bancho). We use clientName here because of #multiplayer and #spectator channels
token.enqueue(serverPackets.channelJoinSuccess(userID, channelObject.clientName))
token.joinChannel(channelObject)
# Send channel joined (IRC)
if glob.irc == True and toIRC == True:
glob.ircServer.banchoJoinChannel(username, channel)
if glob.irc and not toIRC:
glob.ircServer.banchoJoinChannel(token.username, channel)
# Console output
log.info("{} joined channel {}".format(username, channel))
log.info("{} joined channel {}".format(token.username, channel))
# IRC code return
return 0
except exceptions.channelNoPermissionsException:
log.warning("{} attempted to join channel {}, but they have no read permissions".format(username, channel))
log.warning("{} attempted to join channel {}, but they have no read permissions".format(token.username, channel))
return 403
except exceptions.channelUnknownException:
log.warning("{} attempted to join an unknown channel ({})".format(username, channel))
log.warning("{} attempted to join an unknown channel ({})".format(token.username, channel))
return 403
except exceptions.userAlreadyInChannelException:
log.warning("User {} already in channel {}".format(token.username, channel))
return 403
except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho")
return 403 # idk
def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = False):
def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = False, force=False):
"""
Part a channel
userID -- user ID of the user that parts the channel. Optional.
token can be used instead.
token -- user token object of user that parts the channel. Optional.
userID can be used instead.
channel -- name of channel
toIRC -- if True, send this channel join event to IRC. Must be true if joining from bancho.
Optional. Defaukt: True
kick -- if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False
return -- returns 0 if joined or other IRC code in case of error. Needed only on IRC-side
:param userID: user ID of the user that parts the channel. Optional. token can be used instead.
:param token: user token object of user that parts the channel. Optional. userID can be used instead.
:param channel: channel name
:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Optional. Default: True
:param kick: if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False
:param force: whether to allow game clients to part #spect_ and #multi_ channels
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
"""
try:
# Make sure the client is not drunk and sends partChannel when closing a PM tab
if not channel.startswith("#"):
return
# Get token if not defined
if token is None:
token = glob.tokens.getTokenFromUserID(userID)
# Make sure the token exists
if token is None:
raise exceptions.userNotFoundException
raise exceptions.userNotFoundException()
else:
token = token
userID = token.userID
# Get usertoken data
username = token.username
# Determine internal/client name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels)
@ -119,12 +108,24 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
# Make sure the channel exists
if channel not in glob.channels.channels:
raise exceptions.channelUnknownException
raise exceptions.channelUnknownException()
# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually
channelObject = glob.channels.channels[channel]
if channelObject.isSpecial and not token.irc and not force:
raise exceptions.channelUnknownException()
# Make sure the user is in the channel
if channel not in token.joinedChannels:
raise exceptions.userNotInChannelException()
# Part channel (token-side and channel-side)
channelObject = glob.channels.channels[channel]
token.partChannel(channel)
channelObject.userPart(userID)
token.partChannel(channelObject)
# Delete temporary channel if everyone left
if "chat/{}".format(channelObject.name) in glob.streams.streams:
if channelObject.temp and len(glob.streams.streams["chat/{}".format(channelObject.name)].clients) - 1 == 0:
glob.channels.removeChannel(channelObject.name)
# Force close tab if needed
# NOTE: Maybe always needed, will check later
@ -132,17 +133,20 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
token.enqueue(serverPackets.channelKicked(channelClient))
# IRC part
if glob.irc == True and toIRC == True:
glob.ircServer.banchoPartChannel(username, channel)
if glob.irc and toIRC:
glob.ircServer.banchoPartChannel(token.username, channel)
# Console output
log.info("{} parted channel {} ({})".format(username, channel, channelClient))
log.info("{} parted channel {} ({})".format(token.username, channel, channelClient))
# Return IRC code
return 0
except exceptions.channelUnknownException:
log.warning("{} attempted to part an unknown channel ({})".format(username, channel))
log.warning("{} attempted to part an unknown channel ({})".format(token.username, channel))
return 403
except exceptions.userNotInChannelException:
log.warning("{} attempted to part {}, but he's not in that channel".format(token.username, channel))
return 442
except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho")
return 442 # idk
@ -151,35 +155,28 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
"""
Send a message to osu!bancho and IRC server
fro -- sender username. Optional.
You can use token instead of this if you wish.
to -- receiver channel (if starts with #) or username
message -- text of the message
token -- sender token object.
You can use this instead of fro if you are sending messages from bancho.
Optional.
toIRC -- if True, send the message to IRC. If False, send it to Bancho only.
Optional. Default: True
:param fro: sender username. Optional. token can be used instead
:param to: receiver channel (if starts with #) or username
:param message: text of the message
:param token: sender token object. Optional. fro can be used instead
:param toIRC: if True, send the message to IRC. If False, send it to Bancho only. Default: True
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
"""
try:
tokenString = ""
#tokenString = ""
# Get token object if not passed
if token is None:
token = glob.tokens.getTokenFromUsername(fro)
if token is None:
raise exceptions.userNotFoundException
raise exceptions.userNotFoundException()
else:
# token object alredy passed, get its string and its username (fro)
fro = token.username
tokenString = token.token
# Set some variables
userID = token.userID
username = token.username
#tokenString = token.token
# Make sure this is not a tournament client
if token.tournament:
raise exceptions.userTournamentException()
# if token.tournament:
# raise exceptions.userTournamentException()
# Make sure the user is not in restricted mode
if token.restricted:
@ -189,12 +186,16 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
if token.isSilenced():
raise exceptions.userSilencedException()
# Redirect !report to FokaBot
if message.startswith("!report"):
to = glob.BOT_NAME
# Determine internal name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels)
toClient = to
if to == "#spectator":
if token.spectating is None:
s = userID
s = token.userID
else:
s = token.spectatingUserID
to = "#spect_{}".format(s)
@ -205,6 +206,10 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
elif to.startswith("#multi_"):
toClient = "#multiplayer"
# Make sure the message is valid
if not message.strip():
raise exceptions.invalidArgumentsException()
# Truncate message if > 2048 characters
message = message[:2048]+"..." if len(message) > 2048 else message
@ -212,7 +217,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
message = glob.chatFilters.filterMessage(message)
# Build packet bytes
packet = serverPackets.sendMessage(username, toClient, message)
packet = serverPackets.sendMessage(token.username, toClient, message)
# Send the message
isChannel = to.startswith("#")
@ -220,94 +225,113 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# CHANNEL
# Make sure the channel exists
if to not in glob.channels.channels:
raise exceptions.channelUnknownException
raise exceptions.channelUnknownException()
# Make sure the channel is not in moderated mode
if glob.channels.channels[to].moderated == True and token.admin == False:
raise exceptions.channelModeratedException
if glob.channels.channels[to].moderated and not token.admin:
raise exceptions.channelModeratedException()
# Make sure we are in the channel
if to not in token.joinedChannels:
# I'm too lazy to put and test the correct IRC error code here...
# but IRC is not strict at all so who cares
raise exceptions.channelNoPermissionsException()
# Make sure we have write permissions
if glob.channels.channels[to].publicWrite == False and token.admin == False:
raise exceptions.channelNoPermissionsException
if not glob.channels.channels[to].publicWrite and not token.admin:
raise exceptions.channelNoPermissionsException()
# Add message in buffer
token.addMessageInBuffer(to, message)
# Everything seems fine, build recipients list and send packet
recipients = glob.channels.channels[to].getConnectedUsers()[:]
for key, value in glob.tokens.tokens.items():
# Skip our client and irc clients
if key == tokenString or value.irc == True:
continue
# Send to this client if it's connected to the channel
if value.userID in recipients:
value.enqueue(packet)
glob.streams.broadcast("chat/{}".format(to), packet, but=[token.token])
else:
# USER
# Make sure recipient user is connected
recipientToken = glob.tokens.getTokenFromUsername(to)
if recipientToken is None:
raise exceptions.userNotFoundException
raise exceptions.userNotFoundException()
# Make sure the recipient is not a tournament client
if recipientToken.tournament:
raise exceptions.userTournamentException()
#if recipientToken.tournament:
# raise exceptions.userTournamentException()
# Make sure the recipient is not restricted or we are FokaBot
if recipientToken.restricted == True and fro.lower() != "fokabot":
if recipientToken.restricted and fro.lower() != glob.BOT_NAME.lower():
raise exceptions.userRestrictedException()
# TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend
# Away check
if recipientToken.awayCheck(token.userID):
sendMessage(to, fro, "\x01ACTION is away: {}\x01".format(recipientToken.awayMessage))
# Check message templates (mods/admins only)
if message in messageTemplates.templates and token.admin == True:
if message in messageTemplates.templates and token.admin:
sendMessage(fro, to, messageTemplates.templates[message])
# Everything seems fine, send packet
recipientToken.enqueue(packet)
# Send the message to IRC
if glob.irc == True and toIRC == True:
glob.ircServer.banchoMessage(fro, to, message)
if glob.irc and toIRC:
messageSplitInLines = message.encode("latin-1").decode("utf-8").split("\n")
for line in messageSplitInLines:
if line == messageSplitInLines[:1] and line == "":
continue
glob.ircServer.banchoMessage(fro, to, line)
# Spam protection (ignore FokaBot)
if userID > 999:
if token.userID > 999:
token.spamProtection()
# Fokabot message
if isChannel == True or to.lower() == "fokabot":
fokaMessage = fokabot.fokabotResponse(username, to, message)
if isChannel or to.lower() == glob.BOT_NAME.lower():
fokaMessage = fokabot.fokabotResponse(token.username, to, message)
if fokaMessage:
sendMessage("FokaBot", to if isChannel else fro, fokaMessage)
sendMessage(glob.BOT_NAME, to if isChannel else fro, fokaMessage)
# File and discord logs (public chat only)
if to.startswith("#"):
log.chat("{fro} @ {to}: {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))[2:-1]))
if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")):
log.chat("{fro} @ {to}: {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
return 0
except exceptions.userSilencedException:
token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft()))
log.warning("{} tried to send a message during silence".format(username))
log.warning("{} tried to send a message during silence".format(token.username))
return 404
except exceptions.channelModeratedException:
log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(username, to))
log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(token.username, to))
return 404
except exceptions.channelUnknownException:
log.warning("{} tried to send a message to an unknown channel ({})".format(username, to))
log.warning("{} tried to send a message to an unknown channel ({})".format(token.username, to))
return 403
except exceptions.channelNoPermissionsException:
log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(username, to))
log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(token.username, to))
return 404
except exceptions.userRestrictedException:
log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(username, to))
log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(token.username, to))
return 404
except exceptions.userTournamentException:
log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(username, to))
log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(token.username, to))
return 404
except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho")
return 401
except exceptions.invalidArgumentsException:
log.warning("{} tried to send an invalid message to {}".format(token.username, to))
return 404
""" IRC-Bancho Connect/Disconnect/Join/Part interfaces"""
def fixUsernameForBancho(username):
"""
Convert username from IRC format (without spaces) to Bancho format (with spaces)
:param username: username to convert
:return: converted username
"""
# If there are no spaces or underscores in the name
# return it
if " " not in username and "_" not in username:
@ -322,9 +346,22 @@ def fixUsernameForBancho(username):
return username.replace("_", " ")
def fixUsernameForIRC(username):
"""
Convert an username from Bancho format to IRC format (underscores instead of spaces)
:param username: username to convert
:return: converted username
"""
return username.replace(" ", "_")
def IRCConnect(username):
"""
Handle IRC login bancho-side.
Add token and broadcast login packet.
:param username: username
:return:
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
@ -335,6 +372,13 @@ def IRCConnect(username):
log.info("{} logged in from IRC".format(username))
def IRCDisconnect(username):
"""
Handle IRC logout bancho-side.
Remove token and broadcast logout packet.
:param username: username
:return:
"""
token = glob.tokens.getTokenFromUsername(username)
if token is None:
log.warning("{} doesn't exist".format(username))
@ -343,6 +387,13 @@ def IRCDisconnect(username):
log.info("{} disconnected from IRC".format(username))
def IRCJoinChannel(username, channel):
"""
Handle IRC channel join bancho-side.
:param username: username
:param channel: channel name
:return: IRC return code
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
@ -353,8 +404,30 @@ def IRCJoinChannel(username, channel):
return joinChannel(userID, channel)
def IRCPartChannel(username, channel):
"""
Handle IRC channel part bancho-side.
:param username: username
:param channel: channel name
:return: IRC return code
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
return partChannel(userID, channel)
return partChannel(userID, channel)
def IRCAway(username, message):
"""
Handle IRC away command bancho-side.
:param username:
:param message: away message
:return: IRC return code
"""
userID = userUtils.getID(username)
if not userID:
log.warning("{} doesn't exist".format(username))
return
glob.tokens.getTokenFromUserID(userID).awayMessage = message
return 305 if message == "" else 306

View File

@ -5,9 +5,9 @@ class config:
# Check if config.ini exists and load/generate it
def __init__(self, file):
"""
Initialize a config object
Initialize a config file object
file -- filename
:param file: file name
"""
self.config = configparser.ConfigParser()
self.default = True
@ -25,9 +25,9 @@ class config:
# Check if config.ini has all needed the keys
def checkConfig(self):
"""
Check if this config has the required keys
Check is the config file has all required keys
return -- True if valid, False if not
:return: True if valid, False if not valid
"""
try:
# Try to get all the required keys
@ -37,22 +37,27 @@ class config:
self.config.get("db","database")
self.config.get("db","workers")
self.config.get("redis","host")
self.config.get("redis","port")
self.config.get("redis","database")
self.config.get("redis","password")
self.config.get("server","port")
self.config.get("server","threads")
self.config.get("server","gzip")
self.config.get("server","gziplevel")
self.config.get("server","cikey")
self.config.get("mirror","url")
self.config.get("mirror","apikey")
self.config.get("cheesegull", "apiurl")
self.config.get("cheesegull", "apikey")
self.config.get("debug","enable")
self.config.get("debug","packets")
self.config.get("debug","time")
self.config.get("sentry","enable")
self.config.get("sentry","banchodns")
self.config.get("sentry","ircdns")
self.config.get("sentry","banchodsn")
self.config.get("sentry","ircdsn")
self.config.get("discord","enable")
self.config.get("discord","boturl")
@ -68,13 +73,17 @@ class config:
self.config.get("localize","enable")
self.config.get("localize","ipapiurl")
self.config.get("custom", "config")
return True
except:
except configparser.Error:
return False
def generateDefaultConfig(self):
"""
Open and set default keys for that config file
Write a default config file to disk
:return:
"""
# Open config.ini in write mode
f = open(self.fileName, "w")
@ -87,6 +96,12 @@ class config:
self.config.set("db", "database", "ripple")
self.config.set("db", "workers", "4")
self.config.add_section("redis")
self.config.set("redis", "host", "localhost")
self.config.set("redis", "port", "6379")
self.config.set("redis", "database", "0")
self.config.set("redis", "password", "")
self.config.add_section("server")
self.config.set("server", "port", "5001")
self.config.set("server", "threads", "16")
@ -94,9 +109,9 @@ class config:
self.config.set("server", "gziplevel", "6")
self.config.set("server", "cikey", "changeme")
self.config.add_section("mirror")
self.config.set("mirror", "url", "http://storage.ripple.moe")
self.config.set("mirror", "apikey", "anotherkey")
self.config.add_section("cheesegull")
self.config.set("cheesegull", "apiurl", "http://cheesegu.ll/api")
self.config.set("cheesegull", "apikey", "")
self.config.add_section("debug")
self.config.set("debug", "enable", "0")
@ -105,8 +120,8 @@ class config:
self.config.add_section("sentry")
self.config.set("sentry", "enable", "0")
self.config.set("sentry", "banchodns", "")
self.config.set("sentry", "ircdns", "")
self.config.set("sentry", "banchodsn", "")
self.config.set("sentry", "ircdsn", "")
self.config.add_section("discord")
self.config.set("discord", "enable", "0")
@ -114,9 +129,9 @@ class config:
self.config.set("discord", "devgroup", "")
self.config.add_section("datadog")
self.config.set("datadog", "enable")
self.config.set("datadog", "apikey")
self.config.set("datadog", "appkey")
self.config.set("datadog", "enable", "0")
self.config.set("datadog", "apikey", "")
self.config.set("datadog", "appkey", "")
self.config.add_section("irc")
self.config.set("irc", "enable", "1")
@ -127,6 +142,9 @@ class config:
self.config.set("localize", "enable", "1")
self.config.set("localize", "ipapiurl", "http://ip.zxq.co")
self.config.add_section("custom")
self.config.set("custom", "config", "common/config.json")
# Write ini to file and close
self.config.write(f)
f.close()

View File

@ -1,11 +1,12 @@
from common.constants import bcolors
from objects import glob
def printServerStartHeader(asciiArt):
def printServerStartHeader(asciiArt=True):
"""
Print server start header with optional ascii art
Print server start message
asciiArt -- if True, will print ascii art too
:param asciiArt: print BanchoBoat ascii art. Default: True
:return:
"""
if asciiArt:
print("{} _ __".format(bcolors.GREEN))
@ -26,41 +27,52 @@ def printServerStartHeader(asciiArt):
print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{}".format(bcolors.ENDC))
printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN)
printColored("> Common submodule v{}".format(glob.COMMON_VERSION), bcolors.GREEN)
printColored("> Made by the Ripple team", bcolors.GREEN)
printColored("> {}https://git.zxq.co/ripple/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN)
printColored("> {}https://zxq.co/ripple/pep.py".format(bcolors.UNDERLINE), bcolors.GREEN)
printColored("> Custom branch by the osufx team (just Sunpy)", bcolors.GREEN)
printColored("> {}https://github.com/osufx/pep.py".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
Print a string without \n at the end
string -- string to print
:param string: string to print
:return:
"""
print(string, end="")
def printColored(string, color):
"""
Print colored string
Print a colored string
string -- string to print
color -- see bcolors.py
:param string: string to print
:param color: ANSI color code
:return:
"""
print("{}{}{}".format(color, string, bcolors.ENDC))
def printError():
"""
Print error text FOR LOADING
Print a red "Error"
:return:
"""
printColored("Error", bcolors.RED)
def printDone():
"""
Print error text FOR LOADING
Print a green "Done"
:return:
"""
printColored("Done", bcolors.GREEN)
def printWarning():
"""
Print error text FOR LOADING
Print a yellow "Warning"
:return:
"""
printColored("Warning", bcolors.YELLOW)

View File

@ -1,5 +1,4 @@
"""Contains all country codes with their osu numeric code"""
# TODO: Update countries list
countryCodes = {
"LV": 132,
"AD": 3,
@ -255,12 +254,11 @@ countryCodes = {
def getCountryID(code):
"""
Get country ID for osu client
Get osu country ID from country letters
code -- country name abbreviation (eg: US)
return -- country code int
:param code: country letters (eg: US)
:return: country osu code
"""
if code in countryCodes:
return countryCodes[code]
else:
@ -270,10 +268,9 @@ def getCountryLetters(code):
"""
Get country letters from osu country ID
code -- country code int
return -- country name (2 letters) (XX if code not found)
:param code: osu country ID
:return: country letters (XX if not found)
"""
for key, value in countryCodes.items():
if value == code:
return key

View File

@ -7,10 +7,10 @@ from objects import glob
def getCountry(ip):
"""
Get country from IP address
Get country from IP address using geoip api
ip -- IP Address
return -- Country code (2 letters)
:param ip: IP address
:return: country code. XX if invalid.
"""
try:
# Try to get country from Pikolo Aul's Go-Sanic ip API
@ -22,15 +22,15 @@ def getCountry(ip):
def getLocation(ip):
"""
Get latitude and longitude from IP address
Get latitude and longitude from IP address using geoip api
ip -- IP address
return -- [latitude, longitude]
:param ip: IP address
:return: (latitude, longitude)
"""
try:
# Try to get position from Pikolo Aul's Go-Sanic ip API
result = json.loads(urllib.request.urlopen("{}/{}".format(glob.conf.config["localize"]["ipapiurl"], ip), timeout=3).read().decode())["loc"].split(",")
return [float(result[0]), float(result[1])]
return float(result[0]), float(result[1])
except:
log.error("Error in get position")
return [0,0]
return 0, 0

View File

@ -1,15 +1,15 @@
import struct
from constants import dataTypes
def uleb128Encode(num):
cpdef bytearray uleb128Encode(int num):
"""
Encode int -> uleb128
Encode an int to uleb128
num -- int to encode
return -- bytearray with encoded number
:param num: int to encode
:return: bytearray with encoded number
"""
arr = bytearray()
length = 0
cdef bytearray arr = bytearray()
cdef int length = 0
if num == 0:
return bytearray(b"\x00")
@ -23,15 +23,16 @@ def uleb128Encode(num):
return arr
def uleb128Decode(num):
cpdef list uleb128Decode(bytes num):
"""
Decode uleb128 -> int
Decode a uleb128 to int
num -- encoded uleb128
return -- list. [total, length]
:param num: encoded uleb128 int
:return: (total, length)
"""
shift = 0
arr = [0,0] #total, length
cdef int shift = 0
cdef list arr = [0,0] #total, length
cdef int b
while True:
b = num[arr[1]]
@ -43,16 +44,14 @@ def uleb128Decode(num):
return arr
def unpackData(data, dataType):
cpdef unpackData(bytes data, int dataType):
"""
Unpacks data according to dataType
Unpacks a single section of a packet.
data -- bytes array to unpack
dataType -- data type. See dataTypes.py
return -- unpacked bytes
:param data: bytes to unpack
:param dataType: data type
:return: unpacked bytes
"""
# Get right pack Type
if dataType == dataTypes.UINT16:
unpackType = "<H"
@ -76,18 +75,17 @@ def unpackData(data, dataType):
# Unpack
return struct.unpack(unpackType, bytes(data))[0]
def packData(__data, dataType):
cpdef bytes packData(__data, int dataType):
"""
Packs data according to dataType
Packs a single section of a packet.
data -- bytes to pack
dataType -- data type. See dataTypes.py
return -- packed bytes
:param __data: data to pack
:param dataType: data type
:return: packed bytes
"""
data = bytes() # data to return
pack = True # if True, use pack. False only with strings
cdef bytes data = bytes() # data to return
cdef bint pack = True # if True, use pack. False only with strings
cdef str packType
# Get right pack Type
if dataType == dataTypes.BBYTES:
@ -138,23 +136,24 @@ def packData(__data, dataType):
return data
def buildPacket(__packet, __packetData=None):
cpdef bytes buildPacket(int __packet, list __packetData = None):
"""
Build a packet
Builds a packet
packet -- packet id (int)
packetData -- list [[data, dataType], [data, dataType], ...]
return -- packet bytes
:param __packet: packet ID
:param __packetData: packet structure [[data, dataType], [data, dataType], ...]
:return: packet bytes
"""
# Set some variables
# Default argument
if __packetData is None:
__packetData = []
packetData = bytes()
packetLength = 0
packetBytes = bytes()
# Set some variables
cdef bytes packetData = bytes()
cdef int packetLength = 0
cdef bytes packetBytes = bytes()
# Pack packet data
cdef list i
for i in __packetData:
packetData += packData(i[0], i[1])
@ -168,42 +167,43 @@ def buildPacket(__packet, __packetData=None):
packetBytes += packetData # packet data
return packetBytes
def readPacketID(stream):
cpdef int readPacketID(bytes stream):
"""
Read packetID from stream (0-1 bytes)
Read packetID (first two bytes) from a packet
stream -- data stream
return -- packet ID (int)
:param stream: packet bytes
:return: packet ID
"""
return unpackData(stream[0:2], dataTypes.UINT16)
def readPacketLength(stream):
cpdef int readPacketLength(bytes stream):
"""
Read packet length from stream (3-4-5-6 bytes)
Read packet data length (3:7 bytes) from a packet
stream -- data stream
return -- packet length (int)
:param stream: packet bytes
:return: packet data length
"""
return unpackData(stream[3:7], dataTypes.UINT32)
def readPacketData(stream, structure=None, hasFirstBytes = True):
cpdef readPacketData(bytes stream, list structure=None, bint hasFirstBytes = True):
"""
Read packet data from stream according to structure
stream -- data stream
structure -- [[name, dataType], [name, dataType], ...]
hasFirstBytes -- if True, stream has packetID and length bytes.
if False, stream has only packetData.
Optional. Default: True
return -- dictionary. key: name, value: read data
Read packet data from `stream` according to `structure`
:param stream: packet bytes
:param structure: packet structure: [[name, dataType], [name, dataType], ...]
:param hasFirstBytes: if True, `stream` has packetID and length bytes.
if False, `stream` has only packet data. Default: True
:return: {name: unpackedValue, ...}
"""
# Read packet ID (first 2 bytes)
# Default list argument
if structure is None:
structure = []
data = {}
# Read packet ID (first 2 bytes)
cdef dict data = {}
# Skip packet ID and packet length if needed
cdef start, end
if hasFirstBytes:
end = 7
start = 7
@ -212,6 +212,8 @@ def readPacketData(stream, structure=None, hasFirstBytes = True):
start = 0
# Read packet
cdef list i
cdef bint unpack
for i in structure:
start = end
unpack = True
@ -246,7 +248,10 @@ def readPacketData(stream, structure=None, hasFirstBytes = True):
end = start+length[0]+length[1]+1
# Read bytes
data[i[0]] = ''.join(chr(j) for j in stream[start+1+length[1]:end])
#data[i[0]] = ''.join(chr(j) for j in stream[start+1+length[1]:end])
data[i[0]] = ""
for j in stream[start+1+length[1]:end]:
data[i[0]] += chr(j)
elif i[1] == dataTypes.BYTE:
end = start+1
elif i[1] == dataTypes.UINT16 or i[1] == dataTypes.SINT16:

View File

@ -17,6 +17,7 @@ from objects import glob
def dispose():
"""
Perform some clean up. Called on shutdown.
:return:
"""
print("> Disposing server... ")
@ -27,7 +28,7 @@ def runningUnderUnix():
"""
Get if the server is running under UNIX or NT
return --- True if running under UNIX, otherwise False
:return: True if running under UNIX, otherwise False
"""
return True if os.name == "posix" else False
@ -35,12 +36,14 @@ def scheduleShutdown(sendRestartTime, restart, message = "", delay=20):
"""
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
:param sendRestartTime: time (seconds) to wait before sending server restart packets to every client
:param restart: if True, server will restart. if False, server will shudown
:param message: if set, send that message to every client to warn about the shutdown/restart
:param delay: additional restart delay in seconds. Default: 20
:return:
"""
# Console output
log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay))
log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay), "bunker")
log.info("Sending server restart packets in {} seconds...".format(sendRestartTime))
# Send notification if set
@ -61,13 +64,21 @@ def scheduleShutdown(sendRestartTime, restart, message = "", delay=20):
threading.Timer(sendRestartTime+delay, action).start()
def restartServer():
"""Restart pep.py script"""
"""
Restart pep.py
:return:
"""
log.info("Restarting pep.py...")
dispose()
os.execv(sys.executable, [sys.executable] + sys.argv)
def shutdownServer():
"""Shutdown pep.py"""
"""
Shutdown pep.py
:return:
"""
log.info("Shutting down pep.py...")
dispose()
sig = signal.SIGKILL if runningUnderUnix() else signal.CTRL_C_EVENT
@ -77,7 +88,7 @@ def getSystemInfo():
"""
Get a dictionary with some system/server info
return -- ["unix", "connectedUsers", "webServer", "cpuUsage", "totalMemory", "usedMemory", "loadAverage"]
:return: ["unix", "connectedUsers", "webServer", "cpuUsage", "totalMemory", "usedMemory", "loadAverage"]
"""
data = {"unix": runningUnderUnix(), "connectedUsers": len(glob.tokens.tokens), "matches": len(glob.matches.matches)}

View File

@ -17,23 +17,21 @@ import traceback
import raven
from common.log import logUtils as log
from common.ripple import userUtils
from helpers import chatHelper as chat
from objects import glob
class Client:
"""
IRC Client object
"""
__linesep_regexp = re.compile(r"\r?\n")
def __init__(self, server, sock):
"""
Initialize a Client object
server -- server object
sock -- socket connection object
:param server: server object
:param sock: socket connection object
:return:
"""
self.__timestamp = time.time()
self.__readbuffer = ""
@ -47,6 +45,7 @@ class Client:
self.IRCUsername = ""
self.banchoUsername = ""
self.supposedUsername = ""
self.supposedUserID = 0
self.joinedChannels = []
def messageChannel(self, channel, command, message, includeSelf=False):
@ -60,7 +59,8 @@ class Client:
Add a message (basic string) to client buffer.
This is the lowest possible level.
msg -- message to add
:param msg: message to add
:return:
"""
self.__writebuffer += msg + "\r\n"
@ -69,7 +69,7 @@ class Client:
"""
Return this client's write buffer size
return -- write buffer size
:return: write buffer size
"""
return len(self.__writebuffer)
@ -78,7 +78,8 @@ class Client:
"""
Add an IRC-like message to client buffer.
msg -- message (without IRC stuff)
:param msg: message (without IRC stuff)
:return:
"""
self.message(":{} {}".format(self.server.host, msg))
@ -87,10 +88,11 @@ class Client:
"""
Add an IRC-like message to client buffer with code
code -- response code
message -- response message
nickname -- receiver nickname
channel -- optional
:param code: response code
:param message: response message
:param nickname: receiver nickname
:param channel: optional
:return:
"""
if nickname == "":
nickname = self.IRCUsername
@ -103,7 +105,8 @@ class Client:
"""
Add a 403 reply (no such channel) to client buffer.
channel -- meh
:param channel:
:return:
"""
self.replyCode(403, "{} :No such channel".format(channel))
@ -112,7 +115,8 @@ class Client:
"""
Add a 461 reply (not enough parameters) to client buffer
command -- command that had not enough parameters
:param command: name of the command that had not enough parameters
:return:
"""
self.replyCode(403, "{} :Not enough parameters".format(command))
@ -121,8 +125,9 @@ class Client:
"""
Disconnects this client from the IRC server
quitmsg -- IRC quit message. Default: 'Client quit'
callLogout -- if True, call logoutEvent on bancho
:param quitmsg: IRC quit message. Default: 'Client quit'
:param callLogout: if True, call logoutEvent on bancho
:return:
"""
# Send error to client and close socket
self.message("ERROR :{}".format(quitmsg))
@ -133,12 +138,16 @@ class Client:
self.server.removeClient(self, quitmsg)
# Bancho logout
if callLogout:
if callLogout and self.banchoUsername != "":
chat.IRCDisconnect(self.IRCUsername)
def readSocket(self):
"""Read data coming from this client socket"""
"""
Read data coming from this client socket
:return:
"""
try:
# Try to read incoming data from socket
data = self.socket.recv(2 ** 10)
@ -161,7 +170,11 @@ class Client:
def parseBuffer(self):
"""Parse self.__readbuffer, get command, arguments and call its handler"""
"""
Parse self.__readbuffer, get command, arguments and call its handler
:return:
"""
# Get lines from buffer
lines = self.__linesep_regexp.split(self.__readbuffer)
self.__readbuffer = lines[-1]
@ -198,7 +211,11 @@ class Client:
def writeSocket(self):
"""Write buffer to socket"""
"""
Write buffer to socket
:return:
"""
try:
sent = self.socket.send(self.__writebuffer.encode())
log.debug("[IRC] [{}:{}] <- {}".format(self.ip, self.port, self.__writebuffer[:sent]))
@ -206,9 +223,13 @@ class Client:
except socket.error as x:
self.disconnect(str(x))
def checkAlive(self):
"""Check if this client is still connected"""
"""
Check if this client is still connected.
If the client is dead, disconnect it.
:return:
"""
now = time.time()
if self.__timestamp + 180 < now:
self.disconnect("ping timeout")
@ -224,11 +245,19 @@ class Client:
def sendLusers(self):
"""Send lusers response to this client"""
"""
Send lusers response to this client
:return:
"""
self.replyCode(251, "There are {} users and 0 services on 1 server".format(len(glob.tokens.tokens)))
def sendMotd(self):
"""Send MOTD to this client"""
"""
Send MOTD to this client
:return:
"""
self.replyCode(375, "- {} Message of the day - ".format(self.server.host))
if len(self.server.motd) == 0:
self.replyCode(422, "MOTD File is missing")
@ -253,9 +282,10 @@ class Client:
m = hashlib.md5()
m.update(arguments[0].encode("utf-8"))
tokenHash = m.hexdigest()
supposedUsername = glob.db.fetch("SELECT users.username FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1", [tokenHash])
if supposedUsername:
self.supposedUsername = chat.fixUsernameForIRC(supposedUsername["username"])
supposedUser = glob.db.fetch("SELECT users.username, users.id FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1", [tokenHash])
if supposedUser:
self.supposedUsername = chat.fixUsernameForIRC(supposedUser["username"])
self.supposedUserID = supposedUser["id"]
self.__handleCommand = self.registerHandler
else:
# Wrong IRC Token
@ -283,6 +313,11 @@ class Client:
self.reply("464 :Password incorrect")
return
# Make sure that the user is not banned/restricted:
if not userUtils.isAllowed(self.supposedUserID):
self.reply("465 :You're banned")
return
# Make sure we are not connected to Bancho
token = glob.tokens.getTokenFromUsername(chat.fixUsernameForBancho(nick), True)
if token is not None:
@ -296,7 +331,6 @@ class Client:
# Disconnect other IRC clients from the same user
for _, value in self.server.clients.items():
if value.IRCUsername.lower() == self.IRCUsername.lower() and value != self:
print("DISCONNECTERINOOOOOOOOOOOOOOOOOOOOO")
value.disconnect(quitmsg="Connected from another client")
return
elif command == "USER":
@ -324,11 +358,11 @@ class Client:
self.sendMotd()
self.__handleCommand = self.mainHandler
def quitHandler(self, command, arguments):
def quitHandler(self, _, arguments):
"""QUIT command handler"""
self.disconnect(self.IRCUsername if len(arguments) < 1 else arguments[0])
def joinHandler(self, command, arguments):
def joinHandler(self, _, arguments):
"""JOIN command handler"""
if len(arguments) < 1:
self.reply461("JOIN")
@ -341,13 +375,13 @@ class Client:
# TODO: Part all channels
if arguments[0] == "0":
return
'''for (channelname, channel) in self.channels.items():
self.message_channel(channel, "PART", channelname, True)
self.channel_log(channel, "left", meta=True)
server.remove_member_from_channel(self, channelname)
self.channels = {}
return'''
return
# Get channels to join list
channels = arguments[0].split(",")
@ -376,13 +410,15 @@ class Client:
self.replyCode(332, description, channel=channel)
# Build connected users list
users = glob.channels.channels[channel].getConnectedUsers()[:]
if "chat/{}".format(channel) not in glob.streams.streams:
self.reply403(channel)
continue
users = glob.streams.streams["chat/{}".format(channel)].clients
usernames = []
for user in users:
token = glob.tokens.getTokenFromUserID(user)
if token is None:
if user not in glob.tokens.tokens:
continue
usernames.append(chat.fixUsernameForIRC(token.username))
usernames.append(chat.fixUsernameForIRC(glob.tokens.tokens[user].username))
usernames = " ".join(usernames)
# Send IRC users list
@ -393,7 +429,7 @@ class Client:
self.reply403(channel)
continue
def partHandler(self, command, arguments):
def partHandler(self, _, arguments):
"""PART command handler"""
if len(arguments) < 1:
self.reply461("PART")
@ -440,7 +476,6 @@ class Client:
# Send the message to bancho and reply
if not recipientIRC.startswith("#"):
print("PMPMPM!!!!!!!!!!")
recipientBancho = chat.fixUsernameForBancho(recipientIRC)
else:
recipientBancho = recipientIRC
@ -478,7 +513,7 @@ class Client:
"""LUSERS command handler"""
self.sendLusers()
def pingHandler(self, command, arguments):
def pingHandler(self, _, arguments):
"""PING command handler"""
if len(arguments) < 1:
self.replyCode(409, "No origin specified")
@ -489,10 +524,17 @@ class Client:
"""(fake) PONG command handler"""
pass
def awayHandler(self, _, arguments):
"""AWAY command handler"""
response = chat.IRCAway(self.banchoUsername, " ".join(arguments))
self.replyCode(response, "You are no longer marked as being away" if response == 305 else "You have been marked as being away")
def mainHandler(self, command, arguments):
"""Handler for post-login commands"""
"""
Handler for post-login commands
"""
handlers = {
#"AWAY": away_handler,
"AWAY": self.awayHandler,
#"ISON": ison_handler,
"JOIN": self.joinHandler,
#"LIST": list_handler,
@ -518,24 +560,20 @@ class Client:
self.replyCode(421, "Unknown command ({})".format(command))
class Server:
def __init__(self, port):
#self.host = socket.getfqdn("127.0.0.1")[:63]
self.host = glob.conf.config["irc"]["hostname"]
self.port = port
self.clients = {} # Socket --> Client instance.
self.clients = {} # Socket - - > Client instance.
self.motd = ["Welcome to pep.py's embedded IRC server!", "This is a VERY simple IRC server and it's still in beta.", "Expect things to crash and not work as expected :("]
def forceDisconnection(self, username, isBanchoUsername=True):
"""
Disconnect someone from IRC if connected
username -- victim
:param username: victim
:param isBanchoUsername: if True, username is a bancho username, else convert it to a bancho username
:return:
"""
for _, value in self.clients.items():
if (value.IRCUsername == username and not isBanchoUsername) or (value.banchoUsername == username and isBanchoUsername):
@ -546,8 +584,9 @@ class Server:
"""
Let every IRC client connected to a specific client know that 'username' joined the channel from bancho
username -- username of bancho user
channel -- joined channel name
:param username: username of bancho user
:param channel: joined channel name
:return:
"""
username = chat.fixUsernameForIRC(username)
for _, value in self.clients.items():
@ -558,8 +597,9 @@ class Server:
"""
Let every IRC client connected to a specific client know that 'username' parted the channel from bancho
username -- username of bancho user
channel -- joined channel name
:param username: username of bancho user
:param channel: joined channel name
:return:
"""
username = chat.fixUsernameForIRC(username)
for _, value in self.clients.items():
@ -570,9 +610,10 @@ class Server:
"""
Send a message to IRC when someone sends it from bancho
fro -- sender username
to -- receiver username
message -- text of the message
:param fro: sender username
:param to: receiver username
:param message: text of the message
:return:
"""
fro = chat.fixUsernameForIRC(fro)
to = chat.fixUsernameForIRC(to)
@ -588,21 +629,26 @@ class Server:
value.message(":{} PRIVMSG {} :{}".format(fro, to, message))
def removeClient(self, client, quitmsg):
def removeClient(self, client, _):
"""
Remove a client from connected clients
client -- client object
quitmsg -- QUIT argument, useless atm
:param client: client object
:return:
"""
if client.socket in self.clients:
del self.clients[client.socket]
def start(self):
"""Start IRC server main loop"""
"""
Start IRC server main loop
:return:
"""
# Sentry
sentryClient = None
if glob.sentry:
sentryClient = raven.Client(glob.conf.config["sentry"]["ircdns"])
sentryClient = raven.Client(glob.conf.config["sentry"]["ircdsn"])
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -622,7 +668,7 @@ class Server:
[x.socket for x in self.clients.values()
if x.writeBufferSize() > 0],
[],
2)
1)
# Handle incoming connections
for x in iwtd:
@ -633,7 +679,7 @@ class Server:
try:
self.clients[conn] = Client(self, conn)
log.info("[IRC] Accepted connection from {}:{}".format(addr[0], addr[1]))
except socket.error as e:
except socket.error:
try:
conn.close()
except:
@ -652,9 +698,15 @@ class Server:
lastAliveCheck = now
except:
log.error("[IRC] Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
if glob.sentry:
if glob.sentry and sentryClient is not None:
sentryClient.captureException()
def main(port=6667):
"""
Create and start an IRC server
:param port: IRC port. Default: 6667
:return:
"""
glob.ircServer = Server(port)
glob.ircServer.start()

View File

@ -1,6 +1,8 @@
# TODO: Rewrite this shit
from common import generalUtils
from constants import serverPackets
from objects import glob
from common.log import logUtils as log
class banchoConfig:
@ -29,7 +31,12 @@ class banchoConfig:
"""
self.config["banchoMaintenance"] = generalUtils.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'bancho_maintenance'")["value_int"])
self.config["freeDirect"] = generalUtils.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'free_direct'")["value_int"])
self.config["menuIcon"] = glob.db.fetch("SELECT value_string FROM bancho_settings WHERE name = 'menu_icon'")["value_string"]
mainMenuIcon = glob.db.fetch("SELECT file_id, url FROM main_menu_icons WHERE is_current = 1 LIMIT 1")
if mainMenuIcon is None:
self.config["menuIcon"] = ""
else:
imageURL = "https://i.ppy.sh/{}.png".format(mainMenuIcon["file_id"])
self.config["menuIcon"] = "{}|{}".format(imageURL, mainMenuIcon["url"])
self.config["loginNotification"] = glob.db.fetch("SELECT value_string FROM bancho_settings WHERE name = 'login_notification'")["value_string"]
@ -41,3 +48,20 @@ class banchoConfig:
"""
self.config["banchoMaintenance"] = maintenance
glob.db.execute("UPDATE bancho_settings SET value_int = %s WHERE name = 'bancho_maintenance'", [int(maintenance)])
def reload(self):
# Reload settings from bancho_settings
glob.banchoConf.loadSettings()
# Reload channels too
glob.channels.loadChannels()
# And chat filters
glob.chatFilters.loadFilters()
# Send new channels and new bottom icon to everyone
glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
glob.streams.broadcast("main", serverPackets.channelInfoEnd())
for key, value in glob.channels.channels.items():
if value.publicRead and not value.hidden:
glob.streams.broadcast("main", serverPackets.channelInfo(key))

View File

@ -1,20 +1,19 @@
import logging
from constants import exceptions
from objects import glob
class channel:
"""
A chat channel
"""
def __init__(self, name, description, publicRead, publicWrite, temp, hidden):
"""
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
temp -- if True, channel will be deleted when there's no one in the channel
hidden -- if True, channel won't be shown in channels list
:param name: channel name
:param description: channel description
:param publicRead: if True, this channel can be read by everyone. If False, it can be read only by mods/admins
:param publicWrite: same as public read, but regards writing permissions
:param temp: if True, this channel will be deleted when there's no one in this channel
:param hidden: if True, thic channel won't be shown in channels list
"""
self.name = name
self.description = description
@ -22,51 +21,24 @@ class channel:
self.publicWrite = publicWrite
self.moderated = False
self.temp = temp
self.connectedUsers = [999] # Fokabot is always connected to every channels (otherwise it doesn't show up in IRC users list)
self.hidden = hidden
# Client name (#spectator/#multiplayer)
self.clientName = self.name
# Make Foka join the channel
fokaToken = glob.tokens.getTokenFromUserID(999)
if fokaToken is not None:
try:
fokaToken.joinChannel(self)
except exceptions.userAlreadyInChannelException:
logging.warning("FokaBot has already joined channel {}".format(self.name))
@property
def isSpecial(self):
return any(self.name.startswith(x) for x in ("#spect_", "#multi_"))
@property
def clientName(self):
if self.name.startswith("#spect_"):
self.clientName = "#spectator"
return "#spectator"
elif self.name.startswith("#multi_"):
self.clientName = "#multiplayer"
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
"""
if userID in self.connectedUsers:
self.connectedUsers.remove(userID)
# Remove temp channels if empty or there's only fokabot connected
l = len(self.connectedUsers)
if self.temp == True and ((l == 0) or (l == 1 and 999 in self.connectedUsers)):
glob.channels.removeChannel(self.name)
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)
return "#multiplayer"
return self.name

View File

@ -1,21 +1,18 @@
from common.log import logUtils as log
from objects import channel
from objects import glob
from helpers import chatHelper as chat
class channelList:
"""
Channel list
channels -- dictionary. key: channel name, value: channel object
"""
channels = {}
def __init__(self):
self.channels = {}
def loadChannels(self):
"""
Load chat channels from db and add them to channels dictionary
Load chat channels from db and add them to channels list
:return:
"""
# Get channels from DB
channels = glob.db.fetchAll("SELECT * FROM bancho_channels")
@ -28,15 +25,17 @@ class channelList:
def addChannel(self, name, description, publicRead, publicWrite, temp = False, hidden = False):
"""
Add a channel object to channels dictionary
Add a channel to channels list
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
temp -- if True, channel will be deleted when there's no one in the channel. Optional. Default = False.
hidden -- if True, channel will be hidden in channels list. Optional. Default = False.
:param name: channel name
:param description: channel description
:param publicRead: if True, this channel can be read by everyone. If False, it can be read only by mods/admins
:param publicWrite: same as public read, but regards writing permissions
:param temp: if True, this channel will be deleted when there's no one in this channel
:param hidden: if True, thic channel won't be shown in channels list
:return:
"""
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, description, publicRead, publicWrite, temp, hidden)
log.info("Created channel {}".format(name))
@ -45,22 +44,46 @@ class channelList:
Add a temporary channel (like #spectator or #multiplayer), gets deleted when there's no one in the channel
and it's hidden in channels list
name -- channel name
return -- True if channel was created, False if failed
:param name: channel name
:return: True if the channel was created, otherwise False
"""
if name in self.channels:
return False
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, "Chat", True, True, True, True)
log.info("Created temp channel {}".format(name))
def addHiddenChannel(self, name):
"""
Add a hidden channel. It's like a normal channel and must be deleted manually,
but it's not shown in channels list.
:param name: channel name
:return: True if the channel was created, otherwise False
"""
if name in self.channels:
return False
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, "Chat", True, True, False, True)
log.info("Created hidden channel {}".format(name))
def removeChannel(self, name):
"""
Removes a channel from channels list
name -- channel name
:param name: channel name
:return:
"""
if name not in self.channels:
log.debug("{} is not in channels list".format(name))
return
#glob.streams.broadcast("chat/{}".format(name), serverPackets.channelKicked(name))
stream = glob.streams.getStream("chat/{}".format(name))
if stream is not None:
for token in stream.clients:
if token in glob.tokens.tokens:
chat.partChannel(channel=name, token=glob.tokens.tokens[token], kick=True)
glob.streams.dispose("chat/{}".format(name))
glob.streams.remove("chat/{}".format(name))
self.channels.pop(name)
log.info("Removed channel {}".format(name))

View File

@ -1,9 +1,20 @@
class chatFilters:
def __init__(self, fileName="filters.txt"):
"""
Initialize chat filters
:param fileName: name of the file containing filters. Default: filters.txt
"""
self.filters = {}
self.loadFilters(fileName)
def loadFilters(self, fileName="filters.txt"):
"""
Load filters from a file
:param fileName: name of the file containing filters. Default: filters.txt
:return:
"""
# Reset chat filters
self.filters = {}
@ -19,6 +30,14 @@ class chatFilters:
self.filters[lineSplit[0].lower()] = lineSplit[1].replace("\n", "")
def filterMessage(self, message):
"""
Replace forbidden words with filtered ones
:param message: normal message
:return: filtered message
"""
return message
"""
# Split words by spaces
messageTemp = message.split(" ")
@ -32,3 +51,4 @@ class chatFilters:
# Return filtered message
return message
"""

View File

@ -12,30 +12,37 @@ from objects import glob
npRegex = re.compile("^https?:\\/\\/osu\\.ppy\\.sh\\/b\\/(\\d*)")
def connect():
"""Add FokaBot to connected users and send userpanel/stats packet to everyone"""
"""
Connect FokaBot to Bancho
:return:
"""
glob.BOT_NAME = userUtils.getUsername(999)
token = glob.tokens.addToken(999)
token.actionID = actions.IDLE
glob.streams.broadcast("main", serverPackets.userPanel(999))
glob.streams.broadcast("main", serverPackets.userStats(999))
def disconnect():
"""Remove FokaBot from connected users"""
"""
Disconnect FokaBot from Bancho
:return:
"""
glob.tokens.deleteToken(glob.tokens.getTokenFromUserID(999))
def fokabotResponse(fro, chan, message):
"""
Check if a message has triggered fokabot (and return its response)
Check if a message has triggered FokaBot
fro -- sender username (for permissions stuff with admin commands)
chan -- channel name
message -- message
return -- fokabot's response string or False
:param fro: sender username
:param chan: channel name (or receiver username)
:param message: chat mesage
:return: FokaBot's response or False if no response
"""
for i in fokabotCommands.commands:
# Loop though all commands
#if i["trigger"] in message:
if generalUtils.strContains(message, i["trigger"]):
if re.compile("^{}( (.+)?)?$".format(i["trigger"])).match(message.strip()):
# message has triggered a command
# Make sure the user has right permissions

View File

@ -1,7 +1,6 @@
"""Global objects and variables"""
import time
from common.ddog import datadogClient
from common.files import fileBuffer, fileLocks
from objects import channelList
@ -12,28 +11,28 @@ from common.web import schiavo
try:
with open("version") as f:
VERSION = f.read()
VERSION = f.read().strip()
if VERSION == "":
raise
raise Exception
except:
VERSION = "¯\_(xd)_/¯"
VERSION = "Unknown"
DATADOG_PREFIX = "peppy"
BOT_NAME = "FokaBot"
application = None
db = None
redis = None
conf = None
banchoConf = None
tokens = tokenList.tokenList()
channels = channelList.channelList()
matches = matchList.matchList()
restarting = False
fLocks = fileLocks.fileLocks()
fileBuffers = fileBuffer.buffersList()
schiavo = schiavo.schiavo()
dog = datadogClient.datadogClient()
verifiedCache = {}
chatFilters = None
userIDCache = {}
pool = None
ircServer = None
busyThreads = 0
@ -45,8 +44,16 @@ gzip = False
localize = False
sentry = False
irc = False
restarting = False
startTime = int(time.time())
streams = streamList.streamList()
# Additional modifications
COMMON_VERSION_REQ = "1.2.1"
try:
with open("common/version") as f:
COMMON_VERSION = f.read().strip()
except:
COMMON_VERSION = "Unknown"

View File

@ -1,4 +1,9 @@
import copy
import json
import threading
import time
from common.log import logUtils as log
from constants import dataTypes
from constants import matchModModes
@ -13,29 +18,31 @@ from objects import glob
class slot:
def __init__(self):
self.status = slotStatuses.free
self.team = 0
self.status = slotStatuses.FREE
self.team = matchTeams.NO_TEAM
self.userID = -1
self.user = None
self.mods = 0
self.loaded = False
self.skip = False
self.complete = False
self.score = 0
self.failed = False
self.passed = True
class match:
"""Multiplayer match object"""
def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID):
def __init__(self, matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID, isTourney=False):
"""
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
:param matchID: match progressive identifier
:param matchName: match name, string
:param matchPassword: match md5 password. Leave empty for no password
:param beatmapID: beatmap ID
:param beatmapName: beatmap name, string
:param beatmapMD5: beatmap md5 hash, string
:param gameMode: game mode ID. See gameModes.py
:param hostUserID: user id of the host
"""
self.matchID = matchID
self.streamName = "multi/{}".format(self.matchID)
@ -49,11 +56,18 @@ class match:
self.beatmapMD5 = beatmapMD5
self.hostUserID = hostUserID
self.gameMode = gameMode
self.matchScoringType = matchScoringTypes.score # default values
self.matchTeamType = matchTeamTypes.headToHead # default value
self.matchModMode = matchModModes.normal # default value
self.matchScoringType = matchScoringTypes.SCORE # default values
self.matchTeamType = matchTeamTypes.HEAD_TO_HEAD # default value
self.matchModMode = matchModModes.NORMAL # default value
self.seed = 0
self.matchDataCache = bytes()
self.isTourney = isTourney
self.isLocked = False # if True, users can't change slots/teams. Used in tourney matches
self.isStarting = False
self._lock = threading.Lock()
self.createTime = int(time.time())
self.vinseID = None
self.bloodcatAlert = False
# Create all slots and reset them
self.slots = []
@ -65,57 +79,67 @@ class match:
glob.streams.add(self.playingStreamName)
# Create #multiplayer channel
glob.channels.addTempChannel("#multi_{}".format(self.matchID))
glob.channels.addHiddenChannel("#multi_{}".format(self.matchID))
log.info("MPROOM{}: {} match created!".format(self.matchID, "Tourney" if self.isTourney else "Normal"))
def getMatchData(self):
def getMatchData(self, censored = False):
"""
Return binary match data structure for packetHelper
Return binary match data structure for packetHelper
:return:
"""
# General match info
# TODO: Test without safe copy, the error might have been caused by outdated python bytecode cache
safeMatch = copy.deepcopy(self)
# safeMatch = copy.deepcopy(self)
struct = [
[safeMatch.matchID, dataTypes.UINT16],
[int(safeMatch.inProgress), dataTypes.BYTE],
[self.matchID, dataTypes.UINT16],
[int(self.inProgress), dataTypes.BYTE],
[0, dataTypes.BYTE],
[safeMatch.mods, dataTypes.UINT32],
[safeMatch.matchName, dataTypes.STRING],
[safeMatch.matchPassword, dataTypes.STRING],
[safeMatch.beatmapName, dataTypes.STRING],
[safeMatch.beatmapID, dataTypes.UINT32],
[safeMatch.beatmapMD5, dataTypes.STRING],
[self.mods, dataTypes.UINT32],
[self.matchName, dataTypes.STRING]
]
if censored and self.matchPassword:
struct.append(["redacted", dataTypes.STRING])
else:
struct.append([self.matchPassword, dataTypes.STRING])
struct.extend([
[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([safeMatch.slots[i].status, dataTypes.BYTE])
struct.append([self.slots[i].status, dataTypes.BYTE])
# Slot teams, always 16 elements
for i in range(0,16):
struct.append([safeMatch.slots[i].team, dataTypes.BYTE])
struct.append([self.slots[i].team, dataTypes.BYTE])
# Slot user ID. Write only if slot is occupied
for i in range(0,16):
if safeMatch.slots[i].user is not None and safeMatch.slots[i].user in glob.tokens.tokens:
struct.append([glob.tokens.tokens[safeMatch.slots[i].user].userID, dataTypes.UINT32])
if self.slots[i].user is not None and self.slots[i].user in glob.tokens.tokens:
struct.append([glob.tokens.tokens[self.slots[i].user].userID, dataTypes.UINT32])
# Other match data
struct.extend([
[safeMatch.hostUserID, dataTypes.SINT32],
[safeMatch.gameMode, dataTypes.BYTE],
[safeMatch.matchScoringType, dataTypes.BYTE],
[safeMatch.matchTeamType, dataTypes.BYTE],
[safeMatch.matchModMode, dataTypes.BYTE],
[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 safeMatch.matchModMode == matchModModes.freeMod:
if self.matchModMode == matchModModes.FREE_MOD:
for i in range(0,16):
struct.append([safeMatch.slots[i].mods, dataTypes.UINT32])
struct.append([self.slots[i].mods, dataTypes.UINT32])
# Seed idk
# TODO: Implement this, it should be used for mania "random" mod
struct.append([safeMatch.seed, dataTypes.UINT32])
struct.append([self.seed, dataTypes.UINT32])
return struct
@ -123,19 +147,44 @@ class match:
"""
Set room host to newHost and send him host packet
newHost -- new host userID
:param newHost: new host userID
:return:
"""
slotID = self.getUserSlotID(newHost)
if slotID is None or self.slots[slotID].user not in glob.tokens.tokens:
return
return False
token = glob.tokens.tokens[self.slots[slotID].user]
self.hostUserID = newHost
token.enqueue(serverPackets.matchTransferHost())
self.sendUpdates()
log.info("MPROOM{}: {} is now the host".format(self.matchID, token.username))
return True
def removeHost(self):
"""
Removes the host (for tourney matches)
:return:
"""
self.hostUserID = -1
self.sendUpdates()
log.info("MPROOM{}: Removed host".format(self.matchID))
def setSlot(self, slotID, status = None, team = None, user = "", mods = None, loaded = None, skip = None, complete = None):
#self.setSlot(i, slotStatuses.notReady, 0, user, 0)
"""
Set data for a specific slot.
All fields but slotID are optional.
Skipped fields won't be edited.
:param slotID: slot ID
:param status: new status
:param team: new team
:param user: new user id
:param mods: new mods
:param loaded: new loaded status
:param skip: new skip value
:param complete: new completed value
:return:
"""
if status is not None:
self.slots[slotID].status = status
@ -161,8 +210,9 @@ class match:
"""
Set slotID mods. Same as calling setSlot and then sendUpdate
slotID -- slot number
mods -- new mods
:param slotID: slot number
:param mods: new mods
:return:
"""
# Set new slot data and send update
self.setSlot(slotID, mods=mods)
@ -174,32 +224,36 @@ class match:
Switch slotID ready/not ready status
Same as calling setSlot and then sendUpdate
slotID -- slot number
:param slotID: slot number
:return:
"""
# Update ready status and setnd update
if self.slots[slotID].user is None or self.isStarting:
return
oldStatus = self.slots[slotID].status
if oldStatus == slotStatuses.ready:
newStatus = slotStatuses.notReady
if oldStatus == slotStatuses.READY:
newStatus = slotStatuses.NOT_READY
else:
newStatus = slotStatuses.ready
newStatus = slotStatuses.READY
self.setSlot(slotID, newStatus)
self.sendUpdates()
log.info("MPROOM{}: Slot{} changed ready status to {}".format(self.matchID, slotID, self.slots[slotID].status))
def toggleSlotLock(self, slotID):
def toggleSlotLocked(self, slotID):
"""
Lock a slot
Same as calling setSlot and then sendUpdate
slotID -- slot number
:param slotID: slot number
:return:
"""
# Check if slot is already locked
if self.slots[slotID].status == slotStatuses.locked:
newStatus = slotStatuses.free
if self.slots[slotID].status == slotStatuses.LOCKED:
newStatus = slotStatuses.FREE
else:
newStatus = slotStatuses.locked
newStatus = slotStatuses.LOCKED
# Send updated settings to kicked user, so he returns to lobby
# Send updated settings to kicked user, so they will return to the lobby.
if self.slots[slotID].user is not None and self.slots[slotID].user in glob.tokens.tokens:
glob.tokens.tokens[self.slots[slotID].user].enqueue(serverPackets.updateMatch(self.matchID))
@ -208,13 +262,14 @@ class match:
# Send updates to everyone else
self.sendUpdates()
log.info("MPROOM{}: Slot{} {}".format(self.matchID, slotID, "locked" if newStatus == slotStatuses.locked else "unlocked"))
log.info("MPROOM{}: Slot{} {}".format(self.matchID, slotID, "locked" if newStatus == slotStatuses.LOCKED else "unlocked"))
def playerLoaded(self, userID):
"""
Set a player loaded status to True
userID -- ID of user
:param userID: ID of user
:return:
"""
slotID = self.getUserSlotID(userID)
if slotID is None:
@ -228,7 +283,7 @@ class match:
total = 0
loaded = 0
for i in range(0,16):
if self.slots[i].status == slotStatuses.playing:
if self.slots[i].status == slotStatuses.PLAYING:
total+=1
if self.slots[i].loaded:
loaded+=1
@ -237,7 +292,11 @@ class match:
self.allPlayersLoaded()
def allPlayersLoaded(self):
"""Send allPlayersLoaded packet to every playing usr in match"""
"""
Send allPlayersLoaded packet to every playing usr in match
:return:
"""
glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersLoaded())
log.info("MPROOM{}: All players loaded! Match starting...".format(self.matchID))
@ -245,7 +304,8 @@ class match:
"""
Set a player skip status to True
userID -- ID of user
:param userID: ID of user
:return:
"""
slotID = self.getUserSlotID(userID)
if slotID is None:
@ -263,7 +323,7 @@ class match:
total = 0
skipped = 0
for i in range(0,16):
if self.slots[i].status == slotStatuses.playing:
if self.slots[i].status == slotStatuses.PLAYING:
total+=1
if self.slots[i].skip:
skipped+=1
@ -272,15 +332,39 @@ class match:
self.allPlayersSkipped()
def allPlayersSkipped(self):
"""Send allPlayersSkipped packet to every playing usr in match"""
"""
Send allPlayersSkipped packet to every playing usr in match
:return:
"""
glob.streams.broadcast(self.playingStreamName, serverPackets.allPlayersSkipped())
log.info("MPROOM{}: All players have skipped!".format(self.matchID))
def updateScore(self, slotID, score):
"""
Update score for a slot
:param slotID: the slot that the user that is updating their score is in
:param score: the new score to update
:return:
"""
self.slots[slotID].score = score
def updateHP(self, slotID, hp):
"""
Update HP for a slot
:param slotID: the slot that the user that is updating their hp is in
:param hp: the new hp to update
:return:
"""
self.slots[slotID].failed = True if hp == 254 else False
def playerCompleted(self, userID):
"""
Set userID's slot completed to True
userID -- ID of user
:param userID: ID of user
"""
slotID = self.getUserSlotID(userID)
if slotID is None:
@ -294,7 +378,7 @@ class match:
total = 0
completed = 0
for i in range(0,16):
if self.slots[i].status == slotStatuses.playing:
if self.slots[i].status == slotStatuses.PLAYING:
total+=1
if self.slots[i].complete:
completed+=1
@ -303,18 +387,40 @@ class match:
self.allPlayersCompleted()
def allPlayersCompleted(self):
"""Cleanup match stuff and send match end packet to everyone"""
"""
Cleanup match stuff and send match end packet to everyone
:return:
"""
# Collect some info about the match that just ended to send to the api
infoToSend = {
"id": self.matchID,
"name": self.matchName,
"beatmap_id": self.beatmapID,
"mods": self.mods,
"game_mode": self.gameMode,
"scores": {}
}
# Add score info for each player
for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
infoToSend["scores"][glob.tokens.tokens[self.slots[i].user].userID] = {
"score": self.slots[i].score,
"mods": self.slots[i].mods,
"failed": self.slots[i].failed,
"pass": self.slots[i].passed,
"team": self.slots[i].team
}
# Send the info to the api
glob.redis.publish("api:mp_complete_match", json.dumps(infoToSend))
# Reset inProgress
self.inProgress = False
# Reset slots
for i in range(0,16):
if self.slots[i].user is not None 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
self.resetSlots()
# Send match update
self.sendUpdates()
@ -323,16 +429,51 @@ class match:
glob.streams.broadcast(self.streamName, serverPackets.matchComplete())
# Destroy playing stream
glob.streams.dispose(self.playingStreamName)
glob.streams.remove(self.playingStreamName)
# Console output
log.info("MPROOM{}: Match completed".format(self.matchID))
# Set vinse id if needed
chanName = "#multi_{}".format(self.matchID)
if self.vinseID is None:
self.vinseID = (int(time.time()) // (60 * 15)) << 32 | self.matchID
chat.sendMessage("FokaBot", chanName, "Match history available [{} here]".format(
"https://vinse.ripple.moe/match/{}".format(self.vinseID)
))
if not self.bloodcatAlert:
chat.sendMessage(
"FokaBot",
chanName,
"Oh by the way, in case you're playing unranked or broken maps "
"that are now available through ripple's osu!direct, you can "
"type '!bloodcat' in the chat to get a download link for the "
"currently selected map from Bloodcat!"
)
self.bloodcatAlert = True
# If this is a tournament match, then we send a notification in the chat
# saying that the match has completed.
if self.isTourney and (chanName in glob.channels.channels):
chat.sendMessage(glob.BOT_NAME, chanName, "Match has just finished.")
def resetSlots(self):
for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].status == slotStatuses.PLAYING:
self.slots[i].status = slotStatuses.NOT_READY
self.slots[i].loaded = False
self.slots[i].skip = False
self.slots[i].complete = False
self.slots[i].score = 0
self.slots[i].failed = False
self.slots[i].passed = True
def getUserSlotID(self, userID):
"""
Get slot ID occupied by userID
return -- slot id if found, None if user is not in room
:return: slot id if found, None if user is not in room
"""
for i in range(0,16):
if self.slots[i].user is not None and self.slots[i].user in glob.tokens.tokens and glob.tokens.tokens[self.slots[i].user].userID == userID:
@ -343,21 +484,23 @@ class match:
"""
Add someone to users in match
userID -- user id of the user
return -- True if join success, False if fail (room is full)
:param user: user object of the user
:return: True if join success, False if fail (room is full)
"""
# Make sure we're not in this match
for i in range(0,16):
if self.slots[i].user == user.token:
# Set bugged slot to free
self.setSlot(i, slotStatuses.free, 0, None, 0)
self.setSlot(i, slotStatuses.FREE, 0, None, 0)
# Find first free slot
for i in range(0,16):
if self.slots[i].status == slotStatuses.free:
if self.slots[i].status == slotStatuses.FREE:
# Occupy slot
self.setSlot(i, slotStatuses.notReady, 0, user.token, 0)
team = matchTeams.NO_TEAM
if self.matchTeamType == matchTeamTypes.TEAM_VS or self.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
team = matchTeams.RED if i % 2 == 0 else matchTeams.BLUE
self.setSlot(i, slotStatuses.NOT_READY, team, user.token, 0)
# Send updated match data
self.sendUpdates()
@ -368,11 +511,13 @@ class match:
return False
def userLeft(self, user):
def userLeft(self, user, disposeMatch=True):
"""
Remove someone from users in match
userID -- user if of the user
:param user: user object of the user
:param disposeMatch: if `True`, will try to dispose match if there are no users in the room
:return:
"""
# Make sure the user is in room
slotID = self.getUserSlotID(user.userID)
@ -380,13 +525,13 @@ class match:
return
# Set that slot to free
self.setSlot(slotID, slotStatuses.free, 0, None, 0)
self.setSlot(slotID, slotStatuses.FREE, 0, None, 0)
# Check if everyone left
if self.countUsers() == 0:
if self.countUsers() == 0 and disposeMatch and not self.isTourney:
# Dispose match
glob.matches.disposeMatch(self.matchID)
log.info("MPROOM{}: Room disposed".format(self.matchID))
log.info("MPROOM{}: Room disposed because all users left".format(self.matchID))
return
# Check if host left
@ -407,24 +552,29 @@ class match:
"""
Change userID slot to newSlotID
userID -- user that changed slot
newSlotID -- slot id of new slot
:param userID: user that changed slot
:param newSlotID: slot id of new slot
:return:
"""
# Make sure the match is not locked
if self.isLocked or self.isStarting:
return False
# Make sure the user is in room
oldSlotID = self.getUserSlotID(userID)
if oldSlotID is None:
return
return False
# Make sure there is no one inside new slot
if self.slots[newSlotID].user is not None and self.slots[newSlotID].status != slotStatuses.free:
return
if self.slots[newSlotID].user is not None or self.slots[newSlotID].status != slotStatuses.FREE:
return False
# Get old slot data
#oldData = dill.copy(self.slots[oldSlotID])
oldData = copy.deepcopy(self.slots[oldSlotID])
# Free old slot
self.setSlot(oldSlotID, slotStatuses.free, 0, None, 0, False, False, False)
self.setSlot(oldSlotID, slotStatuses.FREE, 0, None, 0, False, False, False)
# Occupy new slot
self.setSlot(newSlotID, oldData.status, oldData.team, oldData.user, oldData.mods)
@ -434,18 +584,16 @@ class match:
# Console output
log.info("MPROOM{}: {} moved to slot {}".format(self.matchID, userID, newSlotID))
return True
def changePassword(self, newPassword):
"""
Change match password to newPassword
newPassword -- new password string
:param newPassword: new password string
:return:
"""
self.matchPassword = newPassword
#if newPassword != "":
# self.matchPassword = generalUtils.stringMd5(newPassword)
#else:
# self.matchPassword = ""
# Send password change to every user in match
glob.streams.broadcast(self.streamName, serverPackets.changeMatchPassword(self.matchPassword))
@ -460,7 +608,8 @@ class match:
"""
Set match global mods
mods -- mods bitwise int thing
:param mods: mods bitwise int thing
:return:
"""
# Set new mods and send update
self.mods = mods
@ -471,8 +620,9 @@ class match:
"""
Set no beatmap status for userID
userID -- ID of user
has -- True if has beatmap, false if not
:param userID: ID of user
:param has: True if has beatmap, false if not
:return:
"""
# Make sure the user is in room
slotID = self.getUserSlotID(userID)
@ -480,7 +630,7 @@ class match:
return
# Set slot
self.setSlot(slotID, slotStatuses.noMap if not has else slotStatuses.notReady)
self.setSlot(slotID, slotStatuses.NO_MAP if not has else slotStatuses.NOT_READY)
# Send updates
self.sendUpdates()
@ -489,7 +639,8 @@ class match:
"""
Transfer host to slotID
slotID -- ID of slot
:param slotID: ID of slot
:return:
"""
# Make sure there is someone in that slot
if self.slots[slotID].user is None or self.slots[slotID].user not in glob.tokens.tokens:
@ -499,19 +650,22 @@ class match:
self.setHost(glob.tokens.tokens[self.slots[slotID].user].userID)
# Send updates
self.sendUpdates()
# self.sendUpdates()
def playerFailed(self, userID):
"""
Send userID's failed packet to everyone in match
userID -- ID of user
:param userID: ID of user
:return:
"""
# Make sure the user is in room
slotID = self.getUserSlotID(userID)
if slotID is None:
return
self.slots[slotID].passed = False
# Send packet to everyone
glob.streams.broadcast(self.playingStreamName, serverPackets.playerFailed(slotID))
@ -522,10 +676,10 @@ class match:
"""
Fro invites to in this match.
fro -- sender userID
to -- receiver userID
:param fro: sender userID
:param to: receiver userID
:return:
"""
# Get tokens
froToken = glob.tokens.getTokenFromUserID(fro)
toToken = glob.tokens.getTokenFromUserID(to)
@ -534,7 +688,7 @@ class match:
# FokaBot is too busy
if to == 999:
chat.sendMessage("FokaBot", froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.")
chat.sendMessage(glob.BOT_NAME, 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)
@ -544,7 +698,7 @@ class match:
"""
Return how many players are in that match
return -- number of users
:return: number of users
"""
c = 0
for i in range(0,16):
@ -552,27 +706,44 @@ class match:
c+=1
return c
def changeTeam(self, userID):
def changeTeam(self, userID, newTeam=None):
"""
Change userID's team
userID -- id of user
:param userID: id of user
:return:
"""
# Make sure this match's mode has teams
if self.matchTeamType != matchTeamTypes.TEAM_VS and self.matchTeamType != matchTeamTypes.TAG_TEAM_VS:
return
# Make sure the match is not locked
if self.isLocked or self.isStarting:
return
# Make sure the user is in room
slotID = self.getUserSlotID(userID)
if slotID is None:
return
# Update slot and send update
newTeam = matchTeams.blue if self.slots[slotID].team == matchTeams.red else matchTeams.red
if newTeam is None:
newTeam = matchTeams.BLUE if self.slots[slotID].team == matchTeams.RED else matchTeams.RED
self.setSlot(slotID, None, newTeam)
self.sendUpdates()
def sendUpdates(self):
"""
Send match updates packet to everyone in lobby and room streams
:return:
"""
self.matchDataCache = serverPackets.updateMatch(self.matchID)
censoredDataCache = serverPackets.updateMatch(self.matchID, censored=True)
if self.matchDataCache is not None:
glob.streams.broadcast(self.streamName, self.matchDataCache)
glob.streams.broadcast("lobby", self.matchDataCache)
if censoredDataCache is not None:
glob.streams.broadcast("lobby", censoredDataCache)
else:
log.error("MPROOM{}: Can't send match update packet, match data is None!!!".format(self.matchID))
@ -580,16 +751,17 @@ class match:
"""
Check if match teams are valid
return -- True if valid, False if invalid
:return: True if valid, False if invalid
:return:
"""
if self.matchTeamType != matchTeamTypes.teamVs or self.matchTeamType != matchTeamTypes.tagTeamVs:
if self.matchTeamType != matchTeamTypes.TEAM_VS and self.matchTeamType != matchTeamTypes.TAG_TEAM_VS:
# 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].user is not None and (self.slots[i].status & slotStatuses.noMap) == 0:
if self.slots[i].user is not None and (self.slots[i].status & slotStatuses.NO_MAP) == 0:
if firstTeam == -1:
firstTeam = self.slots[i].team
elif firstTeam != self.slots[i].team:
@ -600,21 +772,29 @@ class match:
return False
def start(self):
"""
Start the match
:return:
"""
# Remove isStarting timer flag thingie
self.isStarting = False
# Make sure we have enough players
if self.countUsers() < 2 or not self.checkTeams():
return
return False
# Create playing channel
glob.streams.add(self.playingStreamName)
# Change inProgress value
match.inProgress = True
self.inProgress = True
# Set playing to ready players and set load, skip and complete to False
# Make clients join playing stream
for i in range(0, 16):
if (self.slots[i].status & slotStatuses.ready) > 0 and self.slots[i].user in glob.tokens.tokens:
self.slots[i].status = slotStatuses.playing
if self.slots[i].user in glob.tokens.tokens:
self.slots[i].status = slotStatuses.PLAYING
self.slots[i].loaded = False
self.slots[i].skip = False
self.slots[i].complete = False
@ -624,4 +804,86 @@ class match:
glob.streams.broadcast(self.playingStreamName, serverPackets.matchStart(self.matchID))
# Send updates
self.sendUpdates()
self.sendUpdates()
return True
def forceSize(self, matchSize):
for i in range(0, matchSize):
if self.slots[i].status == slotStatuses.LOCKED:
self.toggleSlotLocked(i)
for i in range(matchSize, 16):
if self.slots[i].status != slotStatuses.LOCKED:
self.toggleSlotLocked(i)
def abort(self):
if not self.inProgress:
log.warning("MPROOM{}: Match is not in progress!".format(self.matchID))
return
self.inProgress = False
self.isStarting = False
self.resetSlots()
self.sendUpdates()
glob.streams.broadcast(self.playingStreamName, serverPackets.matchAbort())
glob.streams.dispose(self.playingStreamName)
glob.streams.remove(self.playingStreamName)
log.info("MPROOM{}: Match aborted".format(self.matchID))
def initializeTeams(self):
if self.matchTeamType == matchTeamTypes.TEAM_VS or self.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
# Set teams
for i, _slot in enumerate(self.slots):
_slot.team = matchTeams.RED if i % 2 == 0 else matchTeams.BLUE
else:
# Reset teams
for _slot in self.slots:
_slot.team = matchTeams.NO_TEAM
def resetMods(self):
for _slot in self.slots:
_slot.mods = 0
def resetReady(self):
for _slot in self.slots:
if _slot.status == slotStatuses.READY:
_slot.status = slotStatuses.NOT_READY
def sendReadyStatus(self):
chanName = "#multi_{}".format(self.matchID)
# Make sure match exists before attempting to do anything else
if chanName not in glob.channels.channels:
return
totalUsers = 0
readyUsers = 0
for slot in self.slots:
# Make sure there is a user in this slot
if slot.user is None:
continue
# In this slot there is a user, so we increase the amount of total users
# in this multi room.
totalUsers += 1
if slot.status == slotStatuses.READY:
readyUsers += 1
message = "{} users ready out of {}.".format(readyUsers, totalUsers)
if totalUsers == readyUsers:
message += " All users ready!"
# Check whether there is anyone left in this match.
if totalUsers == 0:
message = "The match is now empty."
chat.sendMessage(glob.BOT_NAME, chanName, message)
def __enter__(self):
# 🌚🌚🌚🌚🌚
self._lock.acquire()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._lock.release()

View File

@ -1,3 +1,8 @@
import threading
import time
from common.sentry import sentry
from constants.exceptions import periodicLoopException
from objects import match
from objects import glob
from constants import serverPackets
@ -9,39 +14,99 @@ class matchList:
self.matches = {}
self.lastID = 1
def createMatch(self, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID):
def createMatch(self, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID, isTourney=False):
"""
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
:param matchName: match name, string
:param matchPassword: match md5 password. Leave empty for no password
:param beatmapID: beatmap ID
:param beatmapName: beatmap name, string
:param beatmapMD5: beatmap md5 hash, string
:param gameMode: game mode ID. See gameModes.py
:param hostUserID: user id of who created the match
:return: match ID
"""
# Add a new match to matches list and create its stream
matchID = self.lastID
self.lastID+=1
self.matches[matchID] = match.match(matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID)
self.matches[matchID] = match.match(matchID, matchName, matchPassword, beatmapID, beatmapName, beatmapMD5, gameMode, hostUserID, isTourney)
return matchID
def disposeMatch(self, matchID):
"""
Destroy match object with id = matchID
matchID -- ID of match to dispose
:param matchID: ID of match to dispose
:return:
"""
# Make sure the match exists
if matchID not in self.matches:
return
# Remove match object and stream
match = self.matches.pop(matchID)
glob.streams.remove(match.streamName)
glob.streams.remove(match.playingStreamName)
# Get match and disconnect all players
_match = self.matches[matchID]
for slot in _match.slots:
_token = glob.tokens.getTokenFromUserID(slot.userID, ignoreIRC=True)
if _token is None:
continue
_match.userLeft(_token, disposeMatch=False) # don't dispose the match twice when we remove all players
# Delete chat channel
glob.channels.removeChannel("#multi_{}".format(_match.matchID))
# Send matchDisposed packet before disposing streams
glob.streams.broadcast(_match.streamName, serverPackets.disposeMatch(_match.matchID))
# Dispose all streams
glob.streams.dispose(_match.streamName)
glob.streams.dispose(_match.playingStreamName)
glob.streams.remove(_match.streamName)
glob.streams.remove(_match.playingStreamName)
# Send match dispose packet to everyone in lobby
glob.streams.broadcast("lobby", serverPackets.disposeMatch(matchID))
glob.streams.broadcast("lobby", serverPackets.disposeMatch(matchID))
del self.matches[matchID]
log.info("MPROOM{}: Room disposed manually".format(_match.matchID))
@sentry.capture()
def cleanupLoop(self):
"""
Start match cleanup loop.
Empty matches that have been created more than 60 seconds ago will get deleted.
Useful when people create useless lobbies with `!mp make`.
The check is done every 30 seconds.
This method starts an infinite loop, call it only once!
:return:
"""
try:
log.debug("Checking empty matches")
t = int(time.time())
emptyMatches = []
exceptions = []
# Collect all empty matches
for key, m in self.matches.items():
if [x for x in m.slots if x.user is not None]:
continue
if t - m.createTime >= 120:
log.debug("Match #{} marked for cleanup".format(m.matchID))
emptyMatches.append(m.matchID)
# Dispose all empty matches
for matchID in emptyMatches:
try:
self.disposeMatch(matchID)
except Exception as e:
exceptions.append(e)
log.error(
"Something wrong happened while disposing a timed out match. Reporting to Sentry when "
"the loop ends."
)
# Re-raise exception if needed
if exceptions:
raise periodicLoopException(exceptions)
finally:
# Schedule a new check (endless loop)
threading.Timer(30, self.cleanupLoop).start()

View File

@ -5,6 +5,7 @@ import uuid
from common.constants import gameModes, actions
from common.log import logUtils as log
from common.ripple import userUtils
from constants import exceptions
from constants import serverPackets
from events import logoutEvent
from helpers import chatHelper as chat
@ -12,31 +13,35 @@ from objects import glob
class token:
def __init__(self, userID, token_ = None, ip ="", irc = False, timeOffset = 0, tournament = False):
"""
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
ip -- client ip. optional.
irc -- if True, set this token as IRC client. optional.
timeOffset -- the time offset from UTC for this user. optional.
:param userID: user associated to this token
:param token_: if passed, set token to that value
if not passed, token will be generated
:param ip: client ip. optional.
:param irc: if True, set this token as IRC client. Default: False.
:param timeOffset: the time offset from UTC for this user. Default: 0.
:param tournament: if True, flag this client as a tournement client. Default: True.
"""
# Set stuff
self.userID = userID
self.username = userUtils.getUsername(self.userID)
self.safeUsername = userUtils.getSafeUsername(self.userID)
self.privileges = userUtils.getPrivileges(self.userID)
self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer") or userUtils.isInPrivilegeGroup(self.userID, "community manager")
self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer")\
or userUtils.isInPrivilegeGroup(self.userID, "community manager")\
or userUtils.isInPrivilegeGroup(self.userID, "chat mod")
self.irc = irc
self.kicked = False
self.restricted = userUtils.isRestricted(self.userID)
self.loginTime = int(time.time())
self.pingTime = self.loginTime
self.timeOffset = timeOffset
self.lock = threading.Lock() # Sync primitive
self.streams = []
self.tournament = tournament
self.messagesBuffer = []
# Default variables
self.spectators = []
@ -51,6 +56,7 @@ class token:
self.country = 0
self.location = [0,0]
self.awayMessage = ""
self.sentAway = []
self.matchID = -1
self.tillerino = [0,0,-1.0] # beatmap, mods, acc
self.silenceEndTime = 0
@ -79,6 +85,11 @@ class token:
else:
self.token = str(uuid.uuid4())
# Locks
self.processingLock = threading.Lock() # Acquired while there's an incoming packet from this user
self._bufferLock = threading.Lock() # Acquired while writing to packets buffer
self._spectLock = threading.RLock()
# Set stats
self.updateCachedStats()
@ -93,51 +104,72 @@ class token:
"""
Add bytes (packets) to queue
bytes -- (packet) bytes to enqueue
:param bytes_: (packet) bytes to enqueue
"""
if not self.irc:
try:
# Acquire the buffer lock
self._bufferLock.acquire()
# Never enqueue for IRC clients or Foka
if self.irc or self.userID < 999:
return
# Avoid memory leaks
if len(bytes_) < 10 * 10 ** 6:
self.queue += bytes_
else:
log.warning("{}'s packets buffer is above 10M!! Lost some data!".format(self.username))
finally:
# Release the buffer lock
self._bufferLock.release()
def resetQueue(self):
"""Resets the queue. Call when enqueued packets have been sent"""
self.queue = bytes()
try:
self._bufferLock.acquire()
self.queue = bytes()
finally:
self._bufferLock.release()
def joinChannel(self, channel):
def joinChannel(self, channelObject):
"""
Add channel to joined channels list
Join a channel
channel -- channel name
:param channelObject: channel object
:raises: exceptions.userAlreadyInChannelException()
exceptions.channelNoPermissionsException()
"""
if channel not in self.joinedChannels:
self.joinedChannels.append(channel)
if channelObject.name in self.joinedChannels:
raise exceptions.userAlreadyInChannelException()
if not channelObject.publicRead and not self.admin:
raise exceptions.channelNoPermissionsException()
self.joinedChannels.append(channelObject.name)
self.joinStream("chat/{}".format(channelObject.name))
self.enqueue(serverPackets.channelJoinSuccess(self.userID, channelObject.clientName))
def partChannel(self, channel):
def partChannel(self, channelObject):
"""
Remove channel from joined channels list
channel -- channel name
:param channelObject: channel object
"""
if channel in self.joinedChannels:
self.joinedChannels.remove(channel)
self.joinedChannels.remove(channelObject.name)
self.leaveStream("chat/{}".format(channelObject.name))
def setLocation(self, location):
def setLocation(self, latitude, longitude):
"""
Set location (latitude and longitude)
Set client location
location -- [latitude, longitude]
:param latitude: latitude
:param longitude: longitude
"""
self.location = location
self.location = (latitude, longitude)
def getLatitude(self):
"""
Get latitude
return -- latitude
:return: latitude
"""
return self.location[0]
@ -145,122 +177,123 @@ class token:
"""
Get longitude
return -- longitude
:return: longitude
"""
return self.location[1]
def startSpectating(self, host):
"""
Set the spectating user to userID
Set the spectating user to userID, join spectator stream and chat channel
and send required packets to host
user -- user object
:param host: host osuToken object
"""
# Stop spectating old client
self.stopSpectating()
try:
self._spectLock.acquire()
# Set new spectator host
self.spectating = host.token
self.spectatingUserID = host.userID
# Stop spectating old client
self.stopSpectating()
# Add us to host's spectator list
host.spectators.append(self.token)
# Set new spectator host
self.spectating = host.token
self.spectatingUserID = host.userID
# Create and join spectator stream
streamName = "spect/{}".format(host.userID)
glob.streams.add(streamName)
self.joinStream(streamName)
host.joinStream(streamName)
# Add us to host's spectator list
host.spectators.append(self.token)
# Send spectator join packet to host
host.enqueue(serverPackets.addSpectator(self.userID))
# Create and join spectator stream
streamName = "spect/{}".format(host.userID)
glob.streams.add(streamName)
self.joinStream(streamName)
host.joinStream(streamName)
# Create and join #spectator (#spect_userid) channel
glob.channels.addTempChannel("#spect_{}".format(host.userID))
chat.joinChannel(token=self, channel="#spect_{}".format(host.userID))
if len(host.spectators) == 1:
# First spectator, send #spectator join to host too
chat.joinChannel(token=host, channel="#spect_{}".format(host.userID))
# Send spectator join packet to host
host.enqueue(serverPackets.addSpectator(self.userID))
# Send fellow spectator join to all clients
glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID))
# Create and join #spectator (#spect_userid) channel
glob.channels.addTempChannel("#spect_{}".format(host.userID))
chat.joinChannel(token=self, channel="#spect_{}".format(host.userID), force=True)
if len(host.spectators) == 1:
# First spectator, send #spectator join to host too
chat.joinChannel(token=host, channel="#spect_{}".format(host.userID), force=True)
# Get current spectators list
for i in host.spectators:
if i != self.token and i in glob.tokens.tokens:
self.enqueue(serverPackets.fellowSpectatorJoined(glob.tokens.tokens[i].userID))
# Send fellow spectator join to all clients
glob.streams.broadcast(streamName, serverPackets.fellowSpectatorJoined(self.userID))
# Log
log.info("{} is spectating {}".format(self.username, host.username))
# Get current spectators list
for i in host.spectators:
if i != self.token and i in glob.tokens.tokens:
self.enqueue(serverPackets.fellowSpectatorJoined(glob.tokens.tokens[i].userID))
# Log
log.info("{} is spectating {}".format(self.username, host.username))
finally:
self._spectLock.release()
def stopSpectating(self):
# Remove our userID from host's spectators
if self.spectating is None:
return
if self.spectating in glob.tokens.tokens:
hostToken = glob.tokens.tokens[self.spectating]
else:
hostToken = None
streamName = "spect/{}".format(self.spectatingUserID)
# Remove us from host's spectators list,
# leave spectator stream
# and end the spectator left packet to host
self.leaveStream(streamName)
if hostToken is not None:
hostToken.spectators.remove(self.token)
hostToken.enqueue(serverPackets.removeSpectator(self.userID))
# and to all other spectators
for i in hostToken.spectators:
if i in glob.tokens.tokens:
glob.tokens.tokens[i].enqueue(serverPackets.fellowSpectatorLeft(self.userID))
# If nobody is spectating the host anymore, close #spectator channel
# and remove host from spect stream too
if len(hostToken.spectators) == 0:
chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True)
hostToken.leaveStream(streamName)
# Part #spectator channel
chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True)
# Console output
log.info("{} is no longer spectating {}".format(self.username, self.spectatingUserID))
# Set our spectating user to 0
self.spectating = None
self.spectatingUserID = 0
def setCountry(self, countryID):
"""
Set country to countryID
Stop spectating, leave spectator stream and channel
and send required packets to host
countryID -- numeric country ID. See countryHelper.py
:return:
"""
self.country = countryID
try:
self._spectLock.acquire()
def getCountry(self):
"""
Get numeric country ID
# Remove our userID from host's spectators
if self.spectating is None or self.spectatingUserID <= 0:
return
if self.spectating in glob.tokens.tokens:
hostToken = glob.tokens.tokens[self.spectating]
else:
hostToken = None
streamName = "spect/{}".format(self.spectatingUserID)
return -- numeric country ID. See countryHelper.py
"""
return self.country
# Remove us from host's spectators list,
# leave spectator stream
# and end the spectator left packet to host
self.leaveStream(streamName)
if hostToken is not None:
hostToken.spectators.remove(self.token)
hostToken.enqueue(serverPackets.removeSpectator(self.userID))
# and to all other spectators
for i in hostToken.spectators:
if i in glob.tokens.tokens:
glob.tokens.tokens[i].enqueue(serverPackets.fellowSpectatorLeft(self.userID))
# If nobody is spectating the host anymore, close #spectator channel
# and remove host from spect stream too
if len(hostToken.spectators) == 0:
chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True, force=True)
hostToken.leaveStream(streamName)
# Console output
log.info("{} is no longer spectating {}. Current spectators: {}".format(self.username, self.spectatingUserID, hostToken.spectators))
# Part #spectator channel
chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True, force=True)
# Set our spectating user to 0
self.spectating = None
self.spectatingUserID = 0
finally:
self._spectLock.release()
def updatePingTime(self):
"""Update latest ping time"""
"""
Update latest ping time to current time
:return:
"""
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, join match stream and channel
matchID -- new match ID
:param matchID: new match ID
:return:
"""
# Make sure the match exists
if matchID not in glob.matches.matches:
@ -285,9 +318,16 @@ class token:
# Set matchID, join stream, channel and send packet
self.matchID = matchID
self.joinStream(match.streamName)
chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID))
chat.joinChannel(token=self, channel="#multi_{}".format(self.matchID), force=True)
self.enqueue(serverPackets.matchJoinSuccess(matchID))
if match.isTourney:
# Alert the user if we have just joined a tourney match
self.enqueue(serverPackets.notification("You are now in a tournament match."))
# If an user joins, then the ready status of the match changes and
# maybe not all users are ready.
match.sendReadyStatus()
def leaveMatch(self):
"""
Leave joined match, match stream and match channel
@ -299,28 +339,37 @@ class token:
return
# Part #multiplayer channel and streams (/ and /playing)
chat.partChannel(token=self, channel="#multi_{}".format(self.matchID), kick=True)
chat.partChannel(token=self, channel="#multi_{}".format(self.matchID), kick=True, force=True)
self.leaveStream("multi/{}".format(self.matchID))
self.leaveStream("multi/{}/playing".format(self.matchID)) # optional
# Set usertoken match to -1
leavingMatchID = self.matchID
self.matchID = -1
# Make sure the match exists
if self.matchID not in glob.matches.matches:
if leavingMatchID not in glob.matches.matches:
return
# The match exists, get object
match = glob.matches.matches[self.matchID]
match = glob.matches.matches[leavingMatchID]
# Set slot to free
match.userLeft(self)
# Set usertoken match to -1
self.matchID = -1
if match.isTourney:
# If an user leaves, then the ready status of the match changes and
# maybe all users are ready. Or maybe nobody is in the match anymore
match.sendReadyStatus()
def kick(self, message="You have been kicked from the server. Please login again.", reason="kick"):
"""
Kick this user from the server
message -- Notification message to send to this user. Optional.
:param message: Notification message to send to this user.
Default: "You have been kicked from the server. Please login again."
:param reason: Kick reason, used in logs. Default: "kick"
:return:
"""
# Send packet to target
log.info("{} has been disconnected. ({})".format(self.username, reason))
@ -329,21 +378,28 @@ class token:
self.enqueue(serverPackets.loginFailed())
# Logout event
logoutEvent.handle(self, None)
logoutEvent.handle(self, deleteToken=self.irc)
def silence(self, seconds, reason, author = 999):
def silence(self, seconds = None, reason = "", author = 999):
"""
Silences this user (db, packet and token)
seconds -- silence length in seconds
reason -- silence reason
author -- userID of who has silenced the target. Optional. Default: 999 (fokabot)
:param seconds: silence length in seconds. If None, get it from db. Default: None
:param reason: silence reason. Default: empty string
:param author: userID of who has silenced the user. Default: 999 (FokaBot)
:return:
"""
# Silence in db and token
self.silenceEndTime = int(time.time())+seconds
userUtils.silence(self.userID, seconds, reason, author)
if seconds is None:
# Get silence expire from db if needed
seconds = max(0, userUtils.getSilenceEnd(self.userID) - int(time.time()))
else:
# Silence in db and token
userUtils.silence(self.userID, seconds, reason, author)
# Send silence packet to target
# Silence token
self.silenceEndTime = int(time.time()) + seconds
# Send silence packet to user
self.enqueue(serverPackets.silenceEndTime(seconds))
# Send silenced packet to everyone else
@ -353,7 +409,8 @@ class token:
"""
Silences the user if is spamming.
increaseSpamRate -- pass True if the user has sent a new message. Optional. Default: True
:param increaseSpamRate: set to True if the user has sent a new message. Default: True
:return:
"""
# Increase the spam rate if needed
if increaseSpamRate:
@ -367,7 +424,7 @@ class token:
"""
Returns True if this user is silenced, otherwise False
return -- True/False
:return: True if this user is silenced, otherwise False
"""
return self.silenceEndTime-int(time.time()) > 0
@ -376,12 +433,16 @@ class token:
Returns the seconds left for this user's silence
(0 if user is not silenced)
return -- silence seconds left
:return: silence seconds left (or 0)
"""
return max(0, self.silenceEndTime-int(time.time()))
def updateCachedStats(self):
"""Update all cached stats for this token"""
"""
Update all cached stats for this token
:return:
"""
stats = userUtils.getUserStats(self.userID, self.gameMode)
log.debug(str(stats))
if stats is None:
@ -394,36 +455,110 @@ class token:
self.gameRank = stats["gameRank"]
self.pp = stats["pp"]
def checkRestricted(self, force=False):
def checkRestricted(self):
"""
Check if this token is restricted. If so, send fokabot message
force -- If True, get restricted value from db.
If false, get the cached one. Optional. Default: False
:return:
"""
if force:
self.restricted = userUtils.isRestricted(self.userID)
oldRestricted = self.restricted
self.restricted = userUtils.isRestricted(self.userID)
if self.restricted:
self.setRestricted()
elif not self.restricted and oldRestricted != self.restricted:
self.resetRestricted()
def checkBanned(self):
"""
Check if this user is banned. If so, disconnect it.
:return:
"""
if userUtils.isBanned(self.userID):
self.enqueue(serverPackets.loginBanned())
logoutEvent.handle(self, deleteToken=False)
def setRestricted(self):
"""
Set this token as restricted, send FokaBot message to user
and send offline packet to everyone
:return:
"""
self.restricted = True
chat.sendMessage("FokaBot",self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.")
chat.sendMessage(glob.BOT_NAME, self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.")
def resetRestricted(self):
"""
Send FokaBot message to alert the user that he has been unrestricted
and he has to log in again.
:return:
"""
chat.sendMessage(glob.BOT_NAME, self.username, "Your account has been unrestricted! Please log in again.")
def joinStream(self, name):
"""
Join a packet stream, or create it if the stream doesn't exist.
:param name: stream name
:return:
"""
glob.streams.join(name, token=self.token)
if name not in self.streams:
self.streams.append(name)
def leaveStream(self, name):
"""
Leave a packets stream
:param name: stream name
:return:
"""
glob.streams.leave(name, token=self.token)
if name in self.streams:
self.streams.remove(name)
def leaveAllStreams(self):
"""
Leave all joined packet streams
:return:
"""
for i in self.streams:
self.leaveStream(i)
self.leaveStream(i)
def awayCheck(self, userID):
"""
Returns True if userID doesn't know that we are away
Returns False if we are not away or if userID already knows we are away
:param userID: original sender userID
:return:
"""
if self.awayMessage == "" or userID in self.sentAway:
return False
self.sentAway.append(userID)
return True
def addMessageInBuffer(self, chan, message):
"""
Add a message in messages buffer (10 messages, truncated at 50 chars).
Used as proof when the user gets reported.
:param chan: channel
:param message: message content
:return:
"""
if len(self.messagesBuffer) > 9:
self.messagesBuffer = self.messagesBuffer[1:]
self.messagesBuffer.append("{time} - {user}@{channel}: {message}".format(time=time.strftime("%H:%M", time.localtime()), user=self.username, channel=chan, message=message[:50]))
def getMessagesBufferString(self):
"""
Get the content of the messages buffer as a string
:return: messages buffer content as a string
"""
return "\n".join(x for x in self.messagesBuffer)

View File

@ -43,15 +43,29 @@ class stream:
log.info("{} has left stream {}".format(token, self.name))
self.clients.remove(token)
def broadcast(self, data):
def broadcast(self, data, but=None):
"""
Send some data to all clients connected to this stream
Send some data to all (or some) clients connected to this stream
:param data: data to send
:param but: array of tokens to ignore. Default: None (send to everyone)
:return:
"""
if but is None:
but = []
for i in self.clients:
if i in glob.tokens.tokens:
if i not in but:
glob.tokens.tokens[i].enqueue(data)
else:
self.removeClient(token=i)
def dispose(self):
"""
Tell every client in this stream to leave the stream
:return:
"""
for i in self.clients:
if i in glob.tokens.tokens:
glob.tokens.tokens[i].enqueue(data)
else:
self.removeClient(token=i)
glob.tokens.tokens[i].leaveStream(self.name)

View File

@ -1,6 +1,7 @@
from objects import stream
from objects import glob
# TODO: use *args and **kwargs
class streamList:
def __init__(self):
self.streams = {}
@ -55,25 +56,39 @@ class streamList:
return
self.streams[streamName].removeClient(client=client, token=token)
def broadcast(self, streamName, data):
def broadcast(self, streamName, data, but=None):
"""
Send some data to all clients in a stream
:param streamName: stream name
:param data: data to send
:param but: array of tokens to ignore. Default: None (send to everyone)
:return:
"""
if streamName not in self.streams:
return
self.streams[streamName].broadcast(data)
self.streams[streamName].broadcast(data, but)
'''def getClients(self, streamName):
def dispose(self, streamName, *args, **kwargs):
"""
Get all clients in a stream
Call `dispose` on `streamName`
:param streamName: name of the stream
:param args:
:param kwargs:
:return:
"""
if streamName not in self.streams:
return
return self.streams[streamName].clients'''
self.streams[streamName].dispose(*args, **kwargs)
def getStream(self, streamName):
"""
Returns streamName's stream object or None if it doesn't exist
:param streamName:
:return:
"""
if streamName in self.streams:
return self.streams[streamName]
return None

View File

@ -1,59 +1,65 @@
import threading
import time
import redis
from common.ripple import userUtils
from common.log import logUtils as log
from common.sentry import sentry
from constants import serverPackets
from constants.exceptions import periodicLoopException
from events import logoutEvent
from objects import glob
from objects import osuToken
class tokenList:
"""
List of connected osu tokens
tokens -- dictionary. key: token string, value: token object
"""
def __init__(self):
"""
Initialize a tokens list
"""
self.tokens = {}
self._lock = threading.Lock()
def __enter__(self):
self._lock.acquire()
def __exit__(self, exc_type, exc_val, exc_tb):
self._lock.release()
def addToken(self, userID, ip = "", irc = False, timeOffset=0, tournament=False):
"""
Add a token object to tokens list
userID -- user id associated to that token
irc -- if True, set this token as IRC client
return -- token object
:param userID: user id associated to that token
:param ip: ip address of the client
:param irc: if True, set this token as IRC client
:param timeOffset: the time offset from UTC for this user. Default: 0.
:param tournament: if True, flag this client as a tournement client. Default: True.
:return: token object
"""
newToken = osuToken.token(userID, ip=ip, irc=irc, timeOffset=timeOffset, tournament=tournament)
self.tokens[newToken.token] = newToken
glob.redis.incr("ripple:online_users")
return newToken
def deleteToken(self, token):
"""
Delete a token from token list if it exists
token -- token string
:param token: token string
:return:
"""
if token in self.tokens:
# Delete session from DB
if self.tokens[token].ip != "":
userUtils.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip)
# Pop token from list
self.tokens.pop(token)
t = self.tokens.pop(token)
del t
glob.redis.decr("ripple:online_users")
def getUserIDFromToken(self, token):
"""
Get user ID from a token
token -- token to find
return -- false if not found, userID if found
:param token: token to find
:return: False if not found, userID if found
"""
# Make sure the token exists
if token not in self.tokens:
@ -62,63 +68,91 @@ class tokenList:
# Get userID associated to that token
return self.tokens[token].userID
def getTokenFromUserID(self, userID, ignoreIRC=False):
def getTokenFromUserID(self, userID, ignoreIRC=False, _all=False):
"""
Get token from a user ID
userID -- user ID to find
return -- False if not found, token object if found
:param userID: user ID to find
:param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:param _all: if True, return a list with all clients that match given username, otherwise return
only the first occurrence.
:return: False if not found, token object if found
"""
# Make sure the token exists
ret = []
for _, value in self.tokens.items():
if value.userID == userID:
if ignoreIRC and value.irc:
continue
return value
if _all:
ret.append(value)
else:
return value
# Return none if not found
return None
# Return full list or None if not found
if _all:
return ret
else:
return None
def getTokenFromUsername(self, username, ignoreIRC=False):
def getTokenFromUsername(self, username, ignoreIRC=False, safe=False, _all=False):
"""
Get token from a username
Get an osuToken object from an username
username -- username to find
return -- False if not found, token object if found
:param username: normal username or safe username
:param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:param safe: if True, username is a safe username,
compare it with token's safe username rather than normal username
:param _all: if True, return a list with all clients that match given username, otherwise return
only the first occurrence.
:return: osuToken object or None
"""
# lowercase
who = username.lower()
who = username.lower() if not safe else username
# Make sure the token exists
ret = []
for _, value in self.tokens.items():
if value.username.lower() == who:
if (not safe and value.username.lower() == who) or (safe and value.safeUsername == who):
if ignoreIRC and value.irc:
continue
return value
if _all:
ret.append(value)
else:
return value
# Return none if not found
return None
# Return full list or None if not found
if _all:
return ret
else:
return None
def deleteOldTokens(self, userID):
"""
Delete old userID's tokens if found
userID -- tokens associated to this user will be deleted
:param userID: tokens associated to this user will be deleted
:return:
"""
# Delete older tokens
delete = []
for key, value in list(self.tokens.items()):
if value.userID == userID:
# Delete this token from the dictionary
self.tokens[key].kick("You have logged in from somewhere else. You can't connect to Bancho/IRC from more than one device at the same time.", "kicked, multiple clients")
#self.tokens[key].kick("You have logged in from somewhere else. You can't connect to Bancho/IRC from more than one device at the same time.", "kicked, multiple clients")
delete.append(self.tokens[key])
for i in delete:
logoutEvent.handle(i)
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
:param packet: packet bytes to enqueue
:param who: userIDs array
:param but: if True, enqueue to everyone but users in `who` array
:return:
"""
for _, value in self.tokens.items():
shouldEnqueue = False
@ -134,70 +168,95 @@ class tokenList:
"""
Enqueue packet(s) to every connected user
packet -- packet bytes to enqueue
:param packet: packet bytes to enqueue
:return:
"""
for _, value in self.tokens.items():
value.enqueue(packet)
def usersTimeoutCheckLoop(self, timeoutTime = 100, checkTime = 100):
@sentry.capture()
def usersTimeoutCheckLoop(self):
"""
Deletes all timed out users.
If called once, will recall after checkTime seconds and so on, forever
Start timed out users disconnect loop.
This function will be called every `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)
:return:
"""
log.debug("Checking timed out clients")
timedOutTokens = [] # timed out users
timeoutLimit = int(time.time())-timeoutTime
for key, value in self.tokens.items():
# Check timeout (fokabot is ignored)
if value.pingTime < timeoutLimit and value.userID != 999 and value.irc == False and value.tournament == False:
# That user has timed out, add to disconnected tokens
# We can't delete it while iterating or items() throws an error
timedOutTokens.append(key)
try:
log.debug("Checking timed out clients")
exceptions = []
timedOutTokens = [] # timed out users
timeoutLimit = int(time.time()) - 100
for key, value in self.tokens.items():
# Check timeout (fokabot is ignored)
if value.pingTime < timeoutLimit and value.userID != 999 and not value.irc and not value.tournament:
# 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:
log.debug("{} timed out!!".format(self.tokens[i].username))
self.tokens[i].enqueue(serverPackets.notification("Your connection to the server timed out."))
logoutEvent.handle(self.tokens[i], None)
# Delete timed out users from self.tokens
# i is token string (dictionary key)
for i in timedOutTokens:
log.debug("{} timed out!!".format(self.tokens[i].username))
self.tokens[i].enqueue(serverPackets.notification("Your connection to the server timed out."))
try:
logoutEvent.handle(self.tokens[i], None)
except Exception as e:
exceptions.append(e)
log.error(
"Something wrong happened while disconnecting a timed out client. Reporting to Sentry "
"when the loop ends."
)
del timedOutTokens
# Schedule a new check (endless loop)
threading.Timer(checkTime, self.usersTimeoutCheckLoop, [timeoutTime, checkTime]).start()
# Re-raise exceptions if needed
if exceptions:
raise periodicLoopException(exceptions)
finally:
# Schedule a new check (endless loop)
threading.Timer(100, self.usersTimeoutCheckLoop).start()
@sentry.capture()
def spamProtectionResetLoop(self):
"""
Reset spam rate every 10 seconds.
Start spam protection reset loop.
Called every 10 seconds.
CALL THIS FUNCTION ONLY ONCE!
"""
# Reset spamRate for every token
for _, value in self.tokens.items():
value.spamRate = 0
# Schedule a new check (endless loop)
threading.Timer(10, self.spamProtectionResetLoop).start()
:return:
"""
try:
# Reset spamRate for every token
for _, value in self.tokens.items():
value.spamRate = 0
finally:
# Schedule a new check (endless loop)
threading.Timer(10, self.spamProtectionResetLoop).start()
def deleteBanchoSessions(self):
"""
Truncate bancho_sessions table.
Remove all `peppy:sessions:*` redis keys.
Call at bancho startup to delete old cached sessions
:return:
"""
glob.db.execute("TRUNCATE TABLE bancho_sessions")
try:
# TODO: Make function or some redis meme
glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:sessions:*")
except redis.RedisError:
pass
def tokenExists(self, username = "", userID = -1):
"""
Check if a token exists (aka check if someone is connected)
username -- Optional.
userID -- Optional.
return -- True if it exists, otherwise False
Check if a token exists
Use username or userid, not both at the same time.
:param username: Optional.
:param userID: Optional.
:return: True if it exists, otherwise False
"""
if userID > -1:
return True if self.getTokenFromUserID(userID) is not None else False
else:
return True if self.getTokenFromUsername(username) is not None else False
return True if self.getTokenFromUsername(username) is not None else False

135
pep.py
View File

@ -1,21 +1,24 @@
"""Hello, pep.py here, ex-owner of ripple and prime minister of Ripwot."""
import os
import sys
import threading
from multiprocessing.pool import ThreadPool
import tornado.gen
import tornado.httpserver
import tornado.ioloop
import tornado.web
from raven.contrib.tornado import AsyncSentryClient
import redis
from common import generalUtils
import json
import shutil
from distutils.version import LooseVersion
from common import generalUtils, agpl
from common.constants import bcolors
from common.db import dbConnector
from common.ddog import datadogClient
from common.log import logUtils as log
from common.ripple import userUtils
from common.redis import pubSub
from common.web import schiavo
from handlers import apiFokabotMessageHandler
from handlers import apiIsOnlineHandler
@ -33,6 +36,13 @@ from objects import banchoConfig
from objects import chatFilters
from objects import fokabot
from objects import glob
from pubSubHandlers import changeUsernameHandler
from pubSubHandlers import disconnectHandler
from pubSubHandlers import banHandler
from pubSubHandlers import notificationHandler
from pubSubHandlers import updateSilenceHandler
from pubSubHandlers import updateStatsHandler
def make_app():
@ -47,7 +57,15 @@ def make_app():
(r"/stress", heavyHandler.handler)
])
if __name__ == "__main__":
# AGPL license agreement
try:
agpl.check_license("ripple", "pep.py")
except agpl.LicenseError as e:
print(str(e))
sys.exit(1)
try:
# Server start
consoleHelper.printServerStartHeader(True)
@ -71,6 +89,34 @@ if __name__ == "__main__":
sys.exit()
else:
consoleHelper.printDone()
# Read additional config file
consoleHelper.printNoNl("> Loading additional config file... ")
try:
if not os.path.isfile(glob.conf.config["custom"]["config"]):
consoleHelper.printWarning()
consoleHelper.printColored("[!] Missing config file at {}; A default one has been generated at this location.".format(glob.conf.config["custom"]["config"]), bcolors.YELLOW)
shutil.copy("common/default_config.json", glob.conf.config["custom"]["config"])
with open(glob.conf.config["custom"]["config"], "r") as f:
glob.conf.extra = json.load(f)
consoleHelper.printDone()
except:
consoleHelper.printWarning()
consoleHelper.printColored("[!] Unable to load custom config at {}".format(glob.conf.config["custom"]["config"]), bcolors.RED)
consoleHelper.printColored("[!] Make sure you have the latest osufx common submodule!", bcolors.RED)
sys.exit()
# Check if running common module is usable
if glob.COMMON_VERSION == "Unknown":
consoleHelper.printWarning()
consoleHelper.printColored("[!] You do not seem to be using osufx's common submodule... nothing will work...", bcolors.RED)
consoleHelper.printColored("[!] You can download or fork the submodule from {}https://github.com/osufx/ripple-python-common".format(bcolors.UNDERLINE), bcolors.RED)
sys.exit()
elif LooseVersion(glob.COMMON_VERSION_REQ) > LooseVersion(glob.COMMON_VERSION):
consoleHelper.printColored("[!] Your common submodule version is below the required version number for this version of pep.py.", bcolors.RED)
consoleHelper.printColored("[!] You are highly adviced to update your common submodule as stability may vary with outdated modules.", bcolors.RED)
# Create data folder if needed
consoleHelper.printNoNl("> Checking folders... ")
@ -82,7 +128,7 @@ if __name__ == "__main__":
# Connect to db
try:
consoleHelper.printNoNl("> Connecting to MySQL database...")
consoleHelper.printNoNl("> Connecting to MySQL database... ")
glob.db = dbConnector.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"]["workers"]))
consoleHelper.printNoNl(" ")
consoleHelper.printDone()
@ -92,6 +138,31 @@ if __name__ == "__main__":
consoleHelper.printColored("[!] Error while connection to database. Please check your config.ini and run the server again", bcolors.RED)
raise
# Connect to redis
try:
consoleHelper.printNoNl("> Connecting to redis... ")
glob.redis = redis.Redis(glob.conf.config["redis"]["host"], glob.conf.config["redis"]["port"], glob.conf.config["redis"]["database"], glob.conf.config["redis"]["password"])
glob.redis.ping()
consoleHelper.printNoNl(" ")
consoleHelper.printDone()
except:
# Exception while connecting to db
consoleHelper.printError()
consoleHelper.printColored("[!] Error while connection to redis. Please check your config.ini and run the server again", bcolors.RED)
raise
# Empty redis cache
try:
# TODO: Make function or some redis meme
glob.redis.set("ripple:online_users", 0)
glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:*")
except redis.exceptions.ResponseError:
# Script returns error if there are no keys starting with peppy:*
pass
# Save peppy version in redis
glob.redis.set("peppy:version", glob.VERSION)
# Load bancho_settings
try:
consoleHelper.printNoNl("> Loading bancho settings from DB... ")
@ -112,7 +183,7 @@ if __name__ == "__main__":
consoleHelper.printNoNl("> Creating threads pool... ")
glob.pool = ThreadPool(int(glob.conf.config["server"]["threads"]))
consoleHelper.printDone()
except:
except ValueError:
consoleHelper.printError()
consoleHelper.printColored("[!] Error while creating threads pool. Please check your config.ini and run the server again", bcolors.RED)
@ -125,6 +196,11 @@ if __name__ == "__main__":
consoleHelper.printColored("[!] Error while loading chat filters. Make sure there is a filters.txt file present", bcolors.RED)
raise
# Start fokabot
consoleHelper.printNoNl("> Connecting bot... ")
fokabot.connect()
consoleHelper.printDone()
# Initialize chat channels
print("> Initializing chat channels... ")
glob.channels.loadChannels()
@ -136,11 +212,6 @@ if __name__ == "__main__":
glob.streams.add("lobby")
consoleHelper.printDone()
# Start fokabot
consoleHelper.printNoNl("> Connecting FokaBot... ")
fokabot.connect()
consoleHelper.printDone()
# Initialize user timeout check loop
consoleHelper.printNoNl("> Initializing user timeout check loop... ")
glob.tokens.usersTimeoutCheckLoop()
@ -151,9 +222,9 @@ if __name__ == "__main__":
glob.tokens.spamProtectionResetLoop()
consoleHelper.printDone()
# Cache user ids
consoleHelper.printNoNl("> Caching user IDs... ")
userUtils.cacheUserIDs()
# Initialize multiplayer cleanup loop
consoleHelper.printNoNl("> Initializing multiplayer cleanup loop... ")
glob.matches.cleanupLoop()
consoleHelper.printDone()
# Localize warning
@ -163,7 +234,7 @@ if __name__ == "__main__":
# Discord
if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]):
glob.schiavo = schiavo.schiavo(glob.conf.config["discord"]["boturl"])
glob.schiavo = schiavo.schiavo(glob.conf.config["discord"]["boturl"], "**pep.py**")
else:
consoleHelper.printColored("[!] Warning! Discord logging is disabled!", bcolors.YELLOW)
@ -187,7 +258,7 @@ if __name__ == "__main__":
try:
glob.sentry = generalUtils.stringToBool(glob.conf.config["sentry"]["enable"])
if glob.sentry:
glob.application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodns"], release=glob.VERSION)
glob.application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodsn"], release=glob.VERSION)
else:
consoleHelper.printColored("[!] Warning! Sentry logging is disabled!", bcolors.YELLOW)
except:
@ -202,6 +273,17 @@ if __name__ == "__main__":
[
datadogClient.periodicCheck("online_users", lambda: len(glob.tokens.tokens)),
datadogClient.periodicCheck("multiplayer_matches", lambda: len(glob.matches.matches)),
#datadogClient.periodicCheck("ram_clients", lambda: generalUtils.getTotalSize(glob.tokens)),
#datadogClient.periodicCheck("ram_matches", lambda: generalUtils.getTotalSize(glob.matches)),
#datadogClient.periodicCheck("ram_channels", lambda: generalUtils.getTotalSize(glob.channels)),
#datadogClient.periodicCheck("ram_file_buffers", lambda: generalUtils.getTotalSize(glob.fileBuffers)),
#datadogClient.periodicCheck("ram_file_locks", lambda: generalUtils.getTotalSize(glob.fLocks)),
#datadogClient.periodicCheck("ram_datadog", lambda: generalUtils.getTotalSize(glob.datadogClient)),
#datadogClient.periodicCheck("ram_verified_cache", lambda: generalUtils.getTotalSize(glob.verifiedCache)),
#datadogClient.periodicCheck("ram_irc", lambda: generalUtils.getTotalSize(glob.ircServer)),
#datadogClient.periodicCheck("ram_tornado", lambda: generalUtils.getTotalSize(glob.application)),
#datadogClient.periodicCheck("ram_db", lambda: generalUtils.getTotalSize(glob.db)),
])
else:
consoleHelper.printColored("[!] Warning! Datadog stats tracking is disabled!", bcolors.YELLOW)
@ -212,26 +294,39 @@ if __name__ == "__main__":
glob.irc = generalUtils.stringToBool(glob.conf.config["irc"]["enable"])
if glob.irc:
# IRC port
ircPort = 0
try:
ircPort = int(glob.conf.config["irc"]["port"])
except:
except ValueError:
consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED)
log.logMessage("**pep.py** IRC server started!", discord="bunker", of="info.txt", stdout=False)
log.logMessage("IRC server started!", discord="bunker", of="info.txt", stdout=False)
consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN)
threading.Thread(target=lambda: ircserver.main(port=ircPort)).start()
else:
consoleHelper.printColored("[!] Warning! IRC server is disabled!", bcolors.YELLOW)
# Server port
serverPort = 0
try:
serverPort = int(glob.conf.config["server"]["port"])
except:
except ValueError:
consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED)
# Server start message and console output
log.logMessage("**pep.py** Server started!", discord="bunker", of="info.txt", stdout=False)
log.logMessage("Server started!", discord="bunker", of="info.txt", stdout=False)
consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN)
# Connect to pubsub channels
pubSub.listener(glob.redis, {
"peppy:disconnect": disconnectHandler.handler(),
"peppy:change_username": changeUsernameHandler.handler(),
"peppy:reload_settings": lambda x: x == b"reload" and glob.banchoConf.reload(),
"peppy:update_cached_stats": updateStatsHandler.handler(),
"peppy:silence": updateSilenceHandler.handler(),
"peppy:ban": banHandler.handler(),
"peppy:notification": notificationHandler.handler(),
}).start()
# Start tornado
glob.application.listen(serverPort)
tornado.ioloop.IOLoop.instance().start()

View File

View File

@ -0,0 +1,18 @@
from common.redis import generalPubSubHandler
from common.ripple import userUtils
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.privileges = userUtils.getPrivileges(userID)
targetToken.checkBanned()
targetToken.checkRestricted()

View File

@ -0,0 +1,50 @@
from common.redis import generalPubSubHandler
from common.ripple import userUtils
from common.log import logUtils as log
from common.constants import actions
from objects import glob
def handleUsernameChange(userID, newUsername, targetToken=None):
try:
userUtils.appendNotes(userID, "Username change: '{}' -> '{}'".format(userUtils.getUsername(userID), newUsername))
userUtils.changeUsername(userID, newUsername=newUsername)
if targetToken is not None:
targetToken.kick("Your username has been changed to {}. Please log in again.".format(newUsername), "username_change")
except userUtils.usernameAlreadyInUseError:
log.rap(999, "Username change: {} is already in use!", through="Bancho")
if targetToken is not None:
targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail")
except userUtils.invalidUsernameError:
log.rap(999, "Username change: {} is not a valid username!", through="Bancho")
if targetToken is not None:
targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail")
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"newUsername": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
# Get the user's token
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is None:
# If the user is offline change username immediately
handleUsernameChange(data["userID"], data["newUsername"])
else:
if targetToken.irc or (targetToken.actionID != actions.PLAYING and targetToken.actionID != actions.MULTIPLAYING):
# If the user is online and he's connected through IRC or he's not playing,
# change username and kick the user immediately
handleUsernameChange(data["userID"], data["newUsername"], targetToken)
else:
# If the user is playing, delay the username change until he submits the score
# On submit modular, lets will send the username change request again
# through redis once the score has been submitted
# The check is performed on bancho logout too, so if the user disconnects
# without submitting a score, the username gets changed on bancho logout
glob.redis.set("ripple:change_username_pending:{}".format(data["userID"]), data["newUsername"])

View File

@ -0,0 +1,18 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"reason": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is not None:
targetToken.kick(data["reason"], "pubsub_kick")

View File

@ -0,0 +1,19 @@
from common.redis import generalPubSubHandler
from objects import glob
from constants import serverPackets
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"message": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is not None:
targetToken.enqueue(serverPackets.notification(data["message"]))

View File

@ -0,0 +1,15 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.silence()

View File

@ -0,0 +1,15 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.updateCachedStats()

View File

@ -1,6 +1,10 @@
requests
tornado
mysqlclient
psutil
raven
bcrypt
requests==2.18.1
tornado==4.4.2
mysqlclient==1.3.9
psutil==5.2.2
raven==5.32.0
bcrypt==3.1.1
dill==0.2.7.1
redis==2.10.5
cython==0.27.3
datadog==0.14.0

17
setup.py Normal file
View File

@ -0,0 +1,17 @@
"""Cython build file"""
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import os
cythonExt = []
for root, dirs, files in os.walk(os.getcwd()):
for file in files:
if file.endswith(".pyx") and ".pyenv" not in root: # im sorry
filePath = os.path.relpath(os.path.join(root, file))
cythonExt.append(Extension(filePath.replace("/", ".")[:-4], [filePath]))
setup(
name = "pep.pyx modules",
ext_modules = cythonize(cythonExt, nthreads = 4),
)

View File

@ -1 +1 @@
1.9.0
1.13.7