diff --git a/zulip_bots/zulip_bots/bots/connect_four/connect_four.conf b/zulip_bots/zulip_bots/bots/connect_four/connect_four.conf new file mode 100644 index 0000000..7f763a3 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/connect_four/connect_four.conf @@ -0,0 +1,2 @@ +[connect_four] +superusers = ["user@example.com"] diff --git a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py index 0c26753..fe6ae49 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py +++ b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py @@ -3,7 +3,7 @@ from zulip_bots.bots.connect_four.controller import ConnectFourModel class ConnectFourMessageHandler(object): tokens = [':blue_circle:', ':red_circle:'] - + def parse_board(self, board): # Header for the top of the board board_str = ':one: :two: :three: :four: :five: :six: :seven:' @@ -19,7 +19,7 @@ class ConnectFourMessageHandler(object): board_str += ':red_circle: ' return board_str - + def get_player_color(self, turn): return self.tokens[turn] @@ -41,16 +41,16 @@ class ConnectFourBotHandler(GameAdapter): or the comptuer in a game of Connect Four ''' - + def __init__(self): game_name = 'Connect Four' bot_name = 'connect_four' - move_help_message = '* To make your move during a game, type\n' + \ - '```move ```' + move_help_message = '* To make your move during a game, type\n' \ + '```move ```' move_regex = 'move (\d)$' model = ConnectFourModel gameMessageHandler = ConnectFourMessageHandler - + super(ConnectFourBotHandler, self).__init__(game_name, bot_name, move_help_message, move_regex, model, gameMessageHandler) handler_class = ConnectFourBotHandler diff --git a/zulip_bots/zulip_bots/bots/connect_four/controller.py b/zulip_bots/zulip_bots/bots/connect_four/controller.py index 46f6a40..ac07a6f 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/controller.py +++ b/zulip_bots/zulip_bots/bots/connect_four/controller.py @@ -17,10 +17,10 @@ class ConnectFourModel(object): [0, 0, 0, 0, 0, 0, 0]] current_board = blank_board - + def parse_move(self, move): return int(move) - 1 - + def update_board(self, board): self.current_board = deepcopy(board) diff --git a/zulip_bots/zulip_bots/bots/connect_four/doc.md b/zulip_bots/zulip_bots/bots/connect_four/doc.md index 69c4448..3525496 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/doc.md +++ b/zulip_bots/zulip_bots/bots/connect_four/doc.md @@ -25,8 +25,12 @@ can only run a single game at a time* ## Setup -The Connect Four Bot does not require a config file or API key. -It can be used without setup. +To set moderators for the bot, modify the connect_four.conf +file as shown: + +superusers = ["user@example.com", "user@example2.com", ...] + +Moderators can run ```force reset``` in case any user abuse the bot ## Usage @@ -61,7 +65,7 @@ another user. player to decline an invitation to play Connect Four against another user. -7. ```withdraw invitation``` : a command that can only be run by the +7. ```cancel game``` : a command that can only be run by the inviter to withdraw their invitation to play. Especially useful if a player does not respond to an invitation for a long period of time. @@ -75,3 +79,7 @@ the user to confirm they wish to forfeit the game. 10. ```confirm quit``` : causes the user that runs this command to forfeit the game. + +11. ```force reset``` : a command that can only be run by the bot +owner and moderators (see 'Usage' for specifying). Destroys any +game currently being run if users are abusing the bot diff --git a/zulip_bots/zulip_bots/bots/connect_four/game_adapter.py b/zulip_bots/zulip_bots/bots/connect_four/game_adapter.py index cfc5f1d..15979df 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/game_adapter.py +++ b/zulip_bots/zulip_bots/bots/connect_four/game_adapter.py @@ -1,11 +1,9 @@ -# @TODO: place bot owner name in config file, allow bot owner to run special commands - import re +import json from copy import deepcopy -# @TODO: allow superusers class InputVerification(object): - def __init__(self, move_regex): + def __init__(self, move_regex, superusers): self.move_regex = move_regex self.verified_commands = { 'waiting': ['start game with computer', 'start game with \w+@\w+\.\w+'], @@ -13,17 +11,16 @@ class InputVerification(object): 'playing': [[move_regex, 'quit', 'confirm quit'], ['quit', 'confirm quit']] } self.all_valid_commands = ['help', 'status', 'start game with computer', 'start game with \w+@\w+\.\w+', - 'withdraw invitation', 'accept', 'decline', self.move_regex, 'quit', 'confirm quit'] - - verified_users = [] + 'withdraw invitation', 'accept', 'decline', self.move_regex, 'quit', 'confirm quit', 'force reset'] + self.superusers = superusers - + verified_users = [] def permission_lacking_message(self, command): return 'Sorry, but you can\'t run the command ```' + command + '```' def update_commands(self, turn): - self.verified_commands['playing'][-1 * turn + 1].remove(self.move_regex) + self.verified_commands['playing'] = [['quit', 'confirm quit'], ['quit', 'confirm quit']] self.verified_commands['playing'][turn].append(self.move_regex) def reset_commands(self): @@ -50,6 +47,9 @@ class InputVerification(object): return self.regex_match_in_array(command_array, command) + def verify_superuser(self, user): + return user in self.superusers + class StateManager(object): def __init__(self, main_bot_handler): self.users = None @@ -331,11 +331,9 @@ class GameAdapter(object): self.move_help_message = move_help_message self.model = model self.gameMessageHandler = gameMessageHandler() - self.inputVerification = InputVerification(move_regex) + self.inputVerification = InputVerification(move_regex, []) def get_stored_data(self): - # @TODO: remove this comment when you create super users - # return self.data # Uncomment and rerun bot to reset data if users are abusing the bot return self.bot_handler.storage.get(self.bot_name) def update_data(self): @@ -343,13 +341,17 @@ class GameAdapter(object): if 'users' in self.data: self.inputVerification.verified_users = self.data['users'] + else: + self.inputVerification.verified_users = [] if self.state == 'inviting': self.invitationHandler = InvitationHandler(self) self.gameHandler = GameHandler(self, self.data['game_type'], self.model()) elif self.state == 'playing': - self.gameHandler = GameHandler(self, self.data['game_type'], self.model(), board = self.data['board'], turn = self.data['turn'], ) + self.gameHandler = GameHandler(self, self.data['game_type'], self.model(), + board = self.data['board'], turn = self.data['turn']) + self.inputVerification.update_commands(self.data['turn']) def put_stored_data(self): self.data = {} @@ -470,11 +472,26 @@ class GameAdapter(object): ''' def initialize(self, bot_handler): + self.config_info = bot_handler.get_config_info('connect_four') + if self.config_info: + self.inputVerification.superusers = json.loads(self.config_info['superusers']) self.gameCreator = GameCreator(self) self.inputVerification.reset_commands() + if not bot_handler.storage.contains(self.bot_name): bot_handler.storage.put(self.bot_name, self.data) + def force_reset(self, sender): + for user in self.inputVerification.verified_users: + self.send_message(user, 'A bot moderator determined you were abusing the bot, and quit your game.' + ' Please make sure you finish all your games in a timely fashion.') + + self.send_message(sender, 'The game has been force reset') + + self.data = data = {'state': 'waiting'} + self.update_data() + self.put_stored_data() + def handle_message(self, message, bot_handler): self.bot_handler = bot_handler @@ -489,7 +506,10 @@ class GameAdapter(object): 'Type ```help``` to see a full list of commands.') return - # Messages that can be sent regardless of state or user + elif self.inputVerification.verify_superuser(sender) and content.lower() == 'force reset': + self.force_reset(sender) + return + elif content.lower() == 'help' or content == '': self.send_message(sender, self.help_message()) return @@ -524,4 +544,4 @@ class GameAdapter(object): self.gameHandler.handle_message(content, sender) self.send_message_arrays(sender) - self.put_stored_data() \ No newline at end of file + self.put_stored_data() diff --git a/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py b/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py index 64c6f55..13d0520 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py +++ b/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py @@ -137,7 +137,7 @@ class TestConnectFourBot(BotTestCase): def test_move(self): self.verify_response('move 8', 'That\'s an invalid move. Please specify a column ' - 'between 1 and 7 with at least one open spot.', 0, data=self.start_two_player_data) + 'between 1 and 7 with at least one open spot.', 0, data=self.start_two_player_data) self.verify_response('move 1', 'You placed your token in column 1.', 0, data=self.start_two_player_data) self.verify_response('move 1', '**the Computer moved in column 1**.', 3, data=self.start_one_player_data, computer_move=0) @@ -149,16 +149,19 @@ class TestConnectFourBot(BotTestCase): def test_quit(self): self.verify_response('quit', 'Are you sure you want to quit? You will forfeit the game!\n' 'Type ```confirm quit``` to forfeit.', 0, data=self.start_two_player_data) - - def test_confirm_quit(self): self.verify_response('confirm quit', '**You have forfeit the game**\nSorry, but you lost :cry:', 0, data=self.start_two_player_data) + def test_force_reset(self): + with self.mock_config_info({'superusers': '["foo@example.com"]'}): + self.verify_response('force reset', 'The game has been force reset', 1, data=self.start_one_player_data) + def test_privilege_check(self): self.verify_response('move 4', 'Sorry, but you can\'t run the command ```move 4```', 0, data=self.inviting_two_player_data) self.verify_response('start game with computer', 'Sorry, but other users are already using the bot.' 'Type ```status``` to see the current status of the bot.', 0, data=self.inviting_two_player_data, user = 'foo3@example.com') self.verify_response('quit', 'Sorry, but you can\'t run the command ```quit```', 0) self.verify_response('accept', 'Sorry, but you can\'t run the command ```accept```', 0, data=self.end_two_player_data) + self.verify_response('force reset', 'Sorry, but you can\'t run the command ```force reset```', 0) def test_connect_four_logic(self): def confirmAvailableMoves(good_moves, bad_moves, board):