def _board(self, board_name): logger.debug("Looking up board {}".format(board_name)) board = [b for b in self.boards if b.name == board_name] try: return board[0] except IndexError as e: raise NoBoardError from e
def store_event(self, event_id, data): logger.debug("Storing event {}".format(event_id)) data = json.dumps(data) sql = "INSERT INTO events (event_id, data) VALUES ('{0}', $${1}$$) ON CONFLICT (event_id) DO UPDATE SET " \ "data=$${1}$$;".format(event_id, data) cur = self.conn.cursor() cur.execute(sql) self.conn.commit()
def userid_info(self, user_id): logger.debug("Looking for user {}".format(user_id)) info = self.sc.api_call("users.info", user=user_id) logger.debug("user info: {}".format(info)) if info["ok"]: return info["user"] else: logger.warn(info["error"])
def _handle_rsvps(self, event): event_id = event["id"] event_name = event["name"] venue = event["venue"]["name"] channel_for_venue = { "STORG Clubhouse": "#storg-south", "STORG Northern Clubhouse": "#storg-north" } channel = channel_for_venue.get(venue) newcomers = [] cancels = [] for rsvp in self.storg.rsvps(event_id): member_name = rsvp["member"]["name"] member_id = rsvp["member"]["member_id"] try: known_participants = self.stored_events[event_id][ "participants"] except KeyError: logger.error("No key {}".format(event_id)) logger.error("Known events: {}".format(self.stored_events)) if member_name not in known_participants and rsvp[ "response"] == "yes": self.trello.add_rsvp(name=member_name, member_id=member_id, board_name=event_name) # self.trello.add_contact(member_name=member_name, member_id=member_id) newcomers.append(member_name) sleep(0.2) elif member_name in known_participants and rsvp["response"] == "no": self.trello.cancel_rsvp(member_id, board_name=event_name) cancels.append(member_name) if newcomers or cancels: spots_left = int(event["rsvp_limit"]) - int( event["yes_rsvp_count"]) if event["rsvp_limit"] else 'Unknown' if newcomers: logger.info("Newcomers found: {}".format(newcomers)) self.chat.new_rsvp(', '.join(newcomers), "yes", event_name, spots_left, channel) self.stored_events[event_id]["participants"] += newcomers logger.debug("Participant list: {}".format( self.stored_events[event_id]["participants"])) if cancels: logger.info("Cancellations found: {}".format(cancels)) self.chat.new_rsvp(', '.join(cancels), "no", event_name, spots_left, channel) self.stored_events[event_id]["participants"] = [ p for p in self.stored_events[event_id]["participants"] if p not in cancels ] logger.debug("Participant list: {}".format( self.stored_events[event_id]["participants"])) else: logger.info("No changes for {}".format(event_name))
def retrieve_event(self, event_id): logger.debug("Retrieving event {}".format(event_id)) if not event_id: return {} sql = "SELECT data FROM events WHERE event_id='{}';".format(event_id) cur = self.conn.cursor() cur.execute(sql) resp = cur.fetchone() return json.dumps(resp)
def add_rsvp(self, name, member_id, board_name): logger.debug("Adding rsvp {} to {}".format(name, board_name)) member_id = str(member_id) board = self._board(board_name) if not board: return None if not self._member(member_id, board_name): rsvp_list = board.list_lists(list_filter="open")[0] rsvp_list.add_card(name=name, desc=member_id)
def _warmup_caches(self): logger.debug("Warming up the caches") ids = self.addressbook try: for meetup_name, slack_name in [(n["name"], n["slack"]) for n in ids.values()]: _ = self.contact_by_name(meetup_name) if slack_name: _ = self.contact_by_slack_name(slack_name) except Exception as e: logger.warning("Exception {} when warming up caches".format(e))
def create_board(self, board_name, team_name=None): logger.debug("Checking for board {} on {} team".format( board_name, team_name)) template = self._board("Meetup Template") org_id = self._org_id(team_name=team_name) try: self._board(board_name) except NoBoardError: self.tc.add_board(board_name=board_name, source_board=template, organization_id=org_id, permission_level="public")
def retrieve_all_events(self): logger.debug("Retrieving all events {}") resp = {} sql = "SELECT event_id, data FROM events;" cur = self.conn.cursor() cur.execute(sql) all_events = self.cur.fetchall() self.conn.commit() for event_id, data in all_events: resp[event_id] = json.loads(data) return resp
def _tables_info(self, channel, request=None, detail=False, table_number=None): logger.debug("Got {} and {}".format(channel, request)) if not request and channel: request = ' '.join(channel.split("_")) logger.debug("Request {}".format(request)) logger.debug("Channel {}".format(channel)) events = self.event_names logger.debug("Events {}".format(events)) event_name = process.extractOne(request, events)[0] logger.debug("Chose {}".format(event_name)) table_info = self.trello.tables_detail(event_name) tables = [] for table, details in table_info.items(): color = "b20000" if table.lower().endswith("full") else "#36a64f" if detail and table[0].isdigit(): text = details["blurb"] title = "Joining ({} out of {} max)".format( len(details["members"]) - 1, details["players"]) elif table[0].isdigit(): text = "_Ask *table {}* to get details for this table " \ "or *detailed table status* to get details for all tables_".format(table[0]) title = "Joining ({} out of {} max)".format( len(details["members"]) - 1, details["players"]) else: text = "" title = "{} left".format(len(details["members"])) color = "" attachment = { "title": table.upper(), "text": text, "color": color, "fields": [{ "title": title, "value": ', '.join(details["members"]) }] } if not table_number or table.startswith(str(table_number)): tables.append(attachment) return json.dumps(tables)
def create_board(self, board_name, team_name=None): logger.debug("Checking for board {} on {} team".format( board_name, team_name)) template = self._board("Meetup Template") board = self._board(board_name) org_id = self._org_id(team_name=team_name) if not board: logger.debug("Adding board {}".format(board_name)) self.tc.add_board(board_name=board_name, source_board=template, organization_id=org_id, permission_level="public")
def retrieve_events(self, event_ids): logger.debug("Retrieving events {}".format(event_ids)) resp = {} if not event_ids: return resp event_ids = ["$${}$$".format(e) for e in event_ids] sql = "SELECT event_id, data FROM events WHERE event_id IN ({});".format(','.join(event_ids)) cur = self.conn.cursor() cur.execute(sql) all_events = cur.fetchall() for event_id, data in all_events: resp[event_id] = json.loads(data) return resp
def contact_by_name(self, member_name): logger.debug("Checking {}".format(member_name)) if self._ab_name_cache.get(member_name): return self._ab_name_cache[member_name] else: board = self._board("Address Book") for l in board.list_lists(list_filter="open"): for card in l.list_cards(): desc = yaml.load(card.desc) if card.name == member_name and desc["slack"]: logger.debug("Desc: {}".format(desc)) self._ab_name_cache[member_name] = yaml.load(card.desc) return self._ab_name_cache[member_name]
def _get(self, path: str, params: dict) -> list: """ Do a GET towards the Meetup API :param path: (str) The path to GET :param params: (dict) Extra parameters to pass to the request :return: (list) The "response" list contained in the Meetup API response """ url = self.api_url + path req = requests.get(url, params) try: return req.json()["results"] except Exception: logger.debug("GET {} failed: {}".format(self.api_url + path, req.headers)) return []
def message(self, content, channel, attachments=None): """Sends a simple message containing :content: to :channel: :param list attachments: :param content: (str) The, well, content of the message :param channel: (str) The channel where to make the announcement. Needs a leading # :return: None """ logger.debug("Sending {} to {}".format(content[0:10], channel)) self.sc.api_call( "chat.postMessage", as_user=True, channel=channel, text=content, attachments=attachments)
def rtm(self, queue, read_delay=1): """Creates a Real Time Messaging connection to Slack and listens for events https://api.slack.com/rtm :param queue: (queue) A Multiprocess Queue where it'll put the incoming events :param read_delay: (int) How often to check for events. Default: 1s :return: None """ if self.sc.rtm_connect(): logger.info("Slack RTM connected") while True: command, channel, user_id = self._parse_slack_output(self.sc.rtm_read()) if command and channel and user_id: logger.debug("command found text: {}, channel: {}, user_id: {}".format(command, channel, user_id)) queue.put((command, channel, user_id)) sleep(read_delay)
def contact_by_slack_name(self, slack_name): if self._ab_slack_cache.get(slack_name): return self._ab_slack_cache[slack_name] else: board = self._board("Address Book") try: for l in board.list_lists(list_filter="open"): for card in l.list_cards(): desc = yaml.load(card.desc) if desc["slack"] == slack_name: self._ab_slack_cache[slack_name] = { "name": card.name, "id": desc["id"] } return self._ab_slack_cache[slack_name] except: logger.debug("Nothing found for {}".format(slack_name))
def _parse_slack_output(self, slack_rtm_output): """Parse the :slack_rtm_output: received from Slack and return everything after the bot's @-name or None if it wasn't directed at the bot. :param slack_rtm_output: (str) Slack message to parse :return: (tuple) A tuple of the striped message and channel id """ output_list = slack_rtm_output if output_list and len(output_list) > 0: for output in output_list: if output and 'text' in output and self.at_bot in output[ 'text'] and output["user"] != 'USLACKBOT' and output[ "ts"]: # return text excluding the @ mention, whitespace removed logger.debug(output) command = ' '.join([ t.strip() for t in output["text"].split(self.at_bot) if t ]) return command, output["channel"], output["user"], output[ "ts"] elif output and "channel" in output and "text" in output \ and self._is_im(output["channel"]) and output["user"] != self.bot_id and \ output["user"] != 'USLACKBOT' and output["ts"]: logger.debug(output) return output["text"], output["channel"], output[ "user"], output["ts"] else: logger.debug(output) return None, None, None, None
def add_rsvp(self, name, member_id, board_name): logger.debug("Adding rsvp {} to {}".format(name, board_name)) try: board = self._board(board_name) except NoBoardError: logger.debug("Board {} not found".format(board_name)) return if not self._member(member_id, board_name): logger.debug("Member {} does not exist in {}. Adding them.".format( member_id, board_name)) rsvp_list = board.list_lists(list_filter="open")[0] logger.debug("RSVP list for {}: {}".format(board_name, rsvp_list)) rsvp_list.add_card(name=name, desc=str(member_id))
def cancel_rsvp(self, member_id, board_name): logger.debug("Cancelling RSVP for members id {} at {}".format( member_id, board_name)) card = self._member(member_id, board_name) logger.debug("Card for member id {} is {}".format(member_id, card)) canceled = self._label("Canceled", board_name) logger.debug("Canceled tag is {}".format(canceled)) if card: card.add_label(canceled)
def __init__(self): meetup_key = environ.get('MEETUP_API_KEY') group_id = environ.get('MEETUP_GROUP_ID') slack_token = environ["SLACK_API_TOKEN"] trello_key = environ["TRELLO_API_KEY"] trello_token = environ["TRELLO_TOKEN"] bot_id = environ.get("BOT_ID") self.lab_channel_id = environ.get("LAB_CHANNEL_ID") self.team_name = environ["TRELLO_TEAM"] self.storg = MeetupGroup(meetup_key, group_id) self.chat = Slack(slack_token, bot_id) self.trello = TrelloBoard(api_key=trello_key, token=trello_token) self.ds = Store() with open("dave/resources/phrases.json", "r") as phrases: self._phrases = json.loads(phrases.read()) if self.storg.upcoming_events: current_event_ids = [e["id"] for e in self.storg.upcoming_events] self.stored_events = self.ds.retrieve_events(current_event_ids) else: self.stored_events = {} logger.debug("Known events: {}".format(self.stored_events)) logger.debug("Env: {}".format(environ.items())) self.chat.message("Reporting for duty!", environ.get("LAB_CHANNEL_ID"))
def conversation(self, task_queue): unknown_responses = self._phrases["responses"]["unknown"] while True: try: command, channel_id, user_id, thread = task_queue.get() attachments = None if command.startswith("help"): response = "Hold on tight, I'm coming!\nJust kidding!\n\n{}".format( self._phrases["responses"]["help"]) thread = None elif command.lower().startswith("table status"): response = "Open tables" attachments = self._tables_info( channel=self.chat.channel_name(channel_id), request=command.split('table status')[-1], only_available=False) elif command.lower().startswith("available tables"): response = "Available tables" attachments = self._tables_info( channel=self.chat.channel_name(channel_id), request=command.split('available tables')[-1], only_available=True) elif command.lower().startswith("detailed table status"): response = "Available tables" attachments = self._tables_info( channel=self.chat.channel_name(channel_id), request=command.split('table status')[-1], detail=True, only_available=False) elif command.lower().startswith("table"): full_req = command.split('table')[-1].strip() split_req = full_req.split(" ", 1) table_number = int(split_req[0]) if len(split_req) == 2: request = split_req[1] else: request = None logger.debug("Table {}".format(table_number)) response = "Details for table {}".format(table_number) attachments = self._tables_info( channel=self.chat.channel_name(channel_id), request=request, detail=True, table_number=table_number) elif "next event" in command.lower( ) and "events" not in command.lower(): response = self._next_event_info() thread = None elif "events" in command.lower(): thread = None response = self._all_events_info() elif "thanks" in command.lower( ) or "thank you" in command.lower(): thread = None response = random.choice( self._phrases["responses"]["thanks"]) elif command.lower().startswith( "what can you do") or command.lower() == "man": thread = None response = self._phrases["responses"]["help"] elif "admin info" in command.lower(): response = self._phrases["responses"]["admin_info"] elif "add table" == command.lower(): response = "Sure thing. Just send me a message in the following format:\n" \ "add table <TABLE TITLE>: <BLURB>, Players: <MAX NUMBER OF PLAYERS>, e.g.\n" \ "```add table Rat Queens (Fate): One more awesome Rat Queens adventure, Players: 5```" elif command.lower().startswith("add table"): response = self._add_table(command, channel_id) else: thread = None response = self._check_for_greeting( command) if self._check_for_greeting( command) else random.choice(unknown_responses) self.respond(response, channel_id, attachments=attachments, thread=thread) except Exception as e: self.chat.message( "Swallowed exception at conversation: {}".format(e), self.lab_channel_id) logger.error( "Swallowed exception at conversation: {}".format(e))
def _tables_info(self, channel, request=None, detail=False, only_available=False, table_number=None): logger.debug("Got {} and {}".format(channel, request)) if not request and channel: request = ' '.join(channel.split("_")) logger.debug("Request {}".format(request)) logger.debug("Channel {}".format(channel)) events = self.storg.event_names logger.debug("Events {}".format(events)) event_name = process.extractOne(request, events)[0] logger.debug("Chose {}".format(event_name)) try: tables_for_event = self.trello.tables_for_event(event_name) except NoBoardError: return "I didn't find anything :disappointed:" tables = [] for table in tables_for_event.values(): color = "b20000" if table.is_full else "#36a64f" title = None if only_available and table.is_full: continue if table_number and table.number != table_number: continue if detail and table.number != 9999: table_title = "{}. {}".format(table.number, table.title) text = table.blurb joining = "*GM:* {}; *Joining ({} out of {} max):* {}".format( table.gm, len(table.players), table.max_players, self._natural_join(table.players, ' ')) elif table.number != 9999: table_title = "{}. {}".format(table.number, table.title) text = "_Ask *table {}* to get details for this table " \ "or *detailed table status* to get details for all tables_".format(table.number) joining = "*GM:* {}; *Joining ({} out of {} max):* {}".format( table.gm, len(table.players), table.max_players, self._natural_join(table.players, ' ')) else: table_title = table.title text = "" color = "" joining = "*{} left:* {}".format( len(table.players), self._natural_join(table.players, ' ')) attachment = { "title": table_title, "text": text, "color": color, "short": True, "fields": [{ "title": title, "value": joining }], "mrkdwn_in": ["text", "pretext", "fields"] } tables.append(attachment) return json.dumps(tables)
def _board(self, board_name): logger.debug("Looking up board {}".format(board_name)) board = [b for b in self.boards if b.name == board_name] if board: return board[0]
def save_events(self): logger.debug("Saving events") self.ds.store_events(self.stored_events)
def store_events(self, events): logger.debug("Storing events {}".format(events)) for event_id, data in events.items(): self.store_event(event_id, data)