def __init__(self, bot, config): EPBotImplant.__init__(self, bot, config) self.vote_rounds = VoteRounds(self.config[self._config_keys[0]]) self._used_regexes = {key: re.compile(rgx) for key, rgx in self._used_regexes.items()}
class VotingRounds(EPBotImplant): """ A voting round control implant. How to start a voting? Examples: 'create poll' will start a dialogue. I will ask for: 1) the topic of the poll, 2) a code to reference the poll and 3) the number of hours it will last. How to vote for a voting round with code *SDT1*? Examples: 'vote SDT1 +1' 'vote SDT1 0' 'vote SDT1 -1' How to get the result for the round? 'show results of voting round SDT1' How to close the round? 'close voting round SDT1' 'close poll SDT1' How to see the polls? 'show open polls' 'show all polls' The round codes must be unique during all the bot's life (or its database). The round codes have a maximum of 10 characters and should be all capital letters or numbers. Generally you can use the term 'voting round' as well as 'poll'. """ _config_keys = ['db_file_path'] _used_regexes = dict([ ('voting_transitives', r'(?P<action>\bcreate \b|\bclose \b|\bshow (?:\bthe \b)?(?:\bresult(?:s)? \b)?\b)(?:\bof \b)?(?:\bthe \b)?(?P<object>\bvoting(?:s)?(?:\b round\b)?\b|\bpoll\b|\b(?:\bopen\b|\ball\b)? poll(?:s)?\b|\b(?:\bopen\b|\ball\b)? voting round(?:s)?\b)(?: )?(?P<poll_code>[A-Z0-9]{1,10})?'), ('vote_value' , r'(?P<action>\bvote\b|\bpoll\b) (?P<poll_code>[A-Z0-9]{1,10}) (?P<vote_value>\-1|0|1|\+1)'), ('vote_round_code' , r'(?P<poll_code>[A-Z0-9]{1,10})'), ('poll_announcement' , r'\A(?:<@.*> )?(?:The poll).*(?:\(\*){1}(?P<poll_code>[A-Z0-9]{1,10})(?:\*\)){1}'), ]) #_used_regexes = ['voting_transitives', 'vote_value', 'vote_round_code'] _transitions = { 'initial': 'idle', # (event name, source state, destination state) 'events' : [('vote_poll', 'idle', 'idle'), ('request_open_polls', 'idle', 'idle'), ('request_all_polls', 'idle', 'idle'), ('request_results', 'idle', 'idle'), # open poll transitions ('open_poll', 'idle', 'ask_topic'), ('receive_topic', 'ask_topic', 'ask_code'), ('topic_error', 'ask_topic', 'ask_topic'), ('receive_code', 'ask_code', 'ask_hours'), ('code_error', 'ask_code', 'ask_code'), ('receive_hours', 'ask_hours', 'ask_open_confirm'), ('hours_error', 'ask_hours', 'ask_hours'), ('confirm_open', 'ask_open_confirm', 'idle'), ('confirm_open_error', 'ask_open_confirm', 'ask_open_confirm'), # close poll transitions ('close_poll', 'idle', 'ask_close_confirm'), ('confirm_close', 'ask_close_confirm', 'idle'), ('confirm_close_error', 'ask_close_confirm', 'ask_close_confirm'), # cancel everything you are doing ('cancel', '*', 'idle')], 'callbacks': {'onvote_poll': on_vote_poll, 'onrequest_open_polls': on_request_open_polls, 'onrequest_all_polls': on_request_all_polls, 'onrequest_results': on_request_results, #'onopen_poll': on_create_poll, 'onask_topic': ask_topic, 'onask_code': ask_code, 'onask_hours': ask_hours, 'onask_open_confirm': confirm_poll_creation, 'onbeforereceive_topic': on_receive_topic, 'onbeforereceive_code': on_receive_code, 'onbeforereceive_hours': on_receive_hours, 'onconfirm_open': on_confirm_open, 'onbeforeclose_poll': on_receive_code, 'onask_close_confirm': confirm_poll_closing, 'onconfirm_close': on_confirm_close, 'ontopic_error': on_error, 'oncode_error': on_error, 'onhours_error': on_error, 'onconfirm_open_error': on_error, 'onconfirm_close_error': on_error, 'oncancel': on_cancel, }} def __init__(self, bot, config): EPBotImplant.__init__(self, bot, config) self.vote_rounds = VoteRounds(self.config[self._config_keys[0]]) self._used_regexes = {key: re.compile(rgx) for key, rgx in self._used_regexes.items()} def save_poll_timestamp(self, poll_match, timestamp): poll_code = get_group_from_match(poll_match, 'poll_code', '') try: self.vote_rounds.set_round_timestamp(poll_code, timestamp) except Exception as exc: #im.add_memo(e.user.name, reply_text=str(exc)) #log.exception('Error `on_vote_poll`.') return str(exc) else: log.debug('Added {} to poll {}.'.format(timestamp, poll_code)) return True def add_vote(self, poll_code, user_id, vote_value): try: self.vote_rounds.insert_vote(poll_code, user_id, vote_value) except Exception as exc: #im.add_memo(e.user.name, reply_text=str(exc)) log.exception('Error `on_vote_poll`.') return str(exc) else: return 'Added vote by <@{}>: `{}` for poll {}.'.format(user_id, vote_value, poll_code) #im.add_memo(e.user.name, reply_text='Added `{}` vote by {} for round {}.'.format(vote_value, e.user.name, # poll_code)) def match_event(self, state, msg): event = None if not state.isstate('idle') and msg == 'cancel': return 'cancel', None if state.isstate('idle'): tran_rgx = self._used_regexes['voting_transitives'] vote_rgx = self._used_regexes['vote_value'] # '(?P<action>\bcreate \b|\bclose \b|\bshow (?:\bresults of \b)?\b)' \ match = tran_rgx.search(msg) if match: action = get_group_from_match(match, 'action', '').strip() object = get_group_from_match(match, 'object', '').strip() poll_code = get_group_from_match(match, 'poll_code', '').strip() if action.startswith('show') and (action.endswith('result') or action.endswith('results')): if 'poll' in object or 'voting' in object: return 'request_results', match elif action.startswith('show'): if 'poll' in object or 'voting' in object: if 'open' in object: return 'request_open_polls', match else: return 'request_all_polls', match elif action.startswith('create'): if 'poll' in object or 'voting' in object: return 'open_poll', match elif action.startswith('close'): if 'poll' in object or 'voting' in object: if poll_code: return 'close_poll', match # match a vote value match = vote_rgx.match(msg) if match: action = get_group_from_match(match, 'action', '') if action.startswith('vote'): return 'vote_poll', match elif state.isstate('ask_topic'): if msg: return 'receive_topic', None else: return 'topic_error', None elif state.isstate('ask_code'): code_rgx = self._used_regexes['vote_round_code'] match = code_rgx.match(msg) if match: return 'receive_code', match else: return 'code_error', None elif state.isstate('ask_hours'): match = re.compile(r"(?P<hours>[0-9]{1,3})").match(msg) if match: return 'receive_hours', match else: return 'hours_error', None elif state.isstate('ask_open_confirm'): if msg == 'yes' or msg == 'no': return 'confirm_open', None else: return 'confirm_open_error', None elif state.isstate('ask_close_confirm'): if msg == 'yes' or msg == 'no': return 'confirm_close', None else: return 'confirm_close_error', None else: event = None match = None return event, match def clear_vote_round_memo(self, user_name): self.pop_memo(user_name, 'round_code') self.pop_memo(user_name, 'topic') self.pop_memo(user_name, 'hours') @asyncio.coroutine def handle_message(self, msg): log.debug('Starting {}'.format(type(self).__name__)) # check if this message is worth checking if not self.check_message(msg): return False # here follows the standard part of the process # cleanup the message text text = cleanup_message_text(msg['text']) # if not direct message, check if it starts with a mention to the bot name if not is_direct_channel(msg['channel']): # Mentions me? has_mention, text = has_initial_mentioning(text, self.rtm.user_id, self.rtm.find_user(self.rtm.user_id).name) if not has_mention: return False # get who is talking to me, aka, user user = self.rtm.find_user(msg['user']) # get the state of the user user_state = self.user_state(user.name) current_state = user_state.current # process the text event, match = self.match_event(user_state, text) if event is None: return False # trigger the state machine try: user_state.trigger(event, msg=text, user=user, implant=self, match=match) except Exception as exc: log.debug('Error triggering event `{}` in implant `{}` with message `{}`, ' 'where user state was `{}`. Exception given: {}'.format(event, type(self).__name__, msg, current_state, str(exc))) return False else: # reply the user if there is any message for him reply_text = self.pop_reply_text(user.name) if reply_text: yield from self.bot.send_message(reply_text, msg['channel'], msg.get('user', None), mkdown=True) return True @asyncio.coroutine def handle_reaction_added(self, event): try: item_ts = event['item']['ts'] poll = self.vote_rounds.find_vote_round_by_timestamp(item_ts) except MoreThanOneVoteRoundFound as mo: raise except: pass else: if poll: reaction = event['reaction'] user_id = event.get('user', None) if reaction in slack_reaction_value: value = slack_reaction_value[reaction] reply = self.add_vote(poll['code'], user_id, value) else: reply = '<@{}> What do you mean by :{}: for poll {}?'.format(user_id, reaction, poll['code']) yield from self.bot.send_message(reply, event['item']['channel'], event.get('user', None), add_mention=False) return True @asyncio.coroutine def handle_reaction_removed(self, event): #TODO pass @asyncio.coroutine def handle_my_own_reply(self, event): announce_rgx = self._used_regexes['poll_announcement'] match = announce_rgx.match(event['text']) if match: self.save_poll_timestamp(poll_match=match, timestamp=event['ts']) return True