def post(self, request, *args, **kwargs): """Handle an inbound HTTP POST request representing a user-issued /command.""" valid, reason = verify_signature(request) if not valid: return HttpResponse(status=401, reason=reason) command = request.POST.get("command") if not command: return HttpResponse("No command specified") command = command.replace("/", "") params = request.POST.get("text", "") context = { "request_scheme": request.scheme, "request_host": request.get_host(), "org_id": request.POST.get("team_id"), "org_name": request.POST.get("team_domain"), "channel_id": request.POST.get("channel_id"), "channel_name": request.POST.get("channel_name"), "user_id": request.POST.get("user_id"), "user_name": request.POST.get("user_name"), "response_url": request.POST.get("response_url"), "trigger_id": request.POST.get("trigger_id"), "integration_url": request.build_absolute_uri( "/api/plugins/chatops/mattermost/interaction/"), "token": request.headers.get("Authorization"), } try: command, subcommand, params = parse_command_string( f"{command} {params}") except ValueError as e: logger.error("%s", e) return HttpResponse( status=400, reason=f"'Error: {e}' encountered on '{command} {params}") registry = get_commands_registry() if command not in registry: MattermostDispatcher(context).send_markdown( commands_help(prefix="/")) return HttpResponse() MattermostDispatcher(context).send_busy_indicator() return check_and_enqueue_command(registry, command, subcommand, params, context, MattermostDispatcher)
def post(self, request, *args, **kwargs): """Handle an inbound HTTP POST request representing a user-issued /command.""" valid, reason = verify_signature(request) if not valid: return HttpResponse(status=401, reason=reason) command = request.POST.get("command") if not command: return HttpResponse("No command specified") command = command.replace(SLASH_PREFIX, "") params = request.POST.get("text", "") context = { "request_scheme": request.scheme, "request_host": request.get_host(), "org_id": request.POST.get("team_id"), "org_name": request.POST.get("team_domain"), "channel_id": request.POST.get("channel_id"), "channel_name": request.POST.get("channel_name"), "user_id": request.POST.get("user_id"), "user_name": request.POST.get("user_name"), "response_url": request.POST.get("response_url"), "trigger_id": request.POST.get("trigger_id"), } try: command, subcommand, params = parse_command_string( f"{command} {params}") except ValueError as e: logger.error("%s", e) # Tried sending 400 error, but the friendly message never made it to slack. return HttpResponse( f"'Error: {e}' encountered on command '{command} {params}'.") registry = get_commands_registry() if command not in registry: SlackDispatcher(context).send_markdown( commands_help(prefix=SLASH_PREFIX)) return HttpResponse() # What we'd like to do here is send a "Nautobot is typing..." to the channel, # but unfortunately the API we're using doesn't support that (only the legacy/deprecated RTM API does). # SlackDispatcher(context).send_busy_indicator() return check_and_enqueue_command(registry, command, subcommand, params, context, SlackDispatcher)
def post(self, request, *args, **kwargs): """Process an inbound HTTP POST request.""" if not API: return HttpResponse(reason="Incomplete or incorrect bot setup") valid, reason = verify_signature(request) if not valid: return HttpResponse(status=401, reason=reason) body = json.loads(request.body) if body.get("resource") not in ["messages", "attachmentActions" ] or body.get("event") != "created": return HttpResponse( reason= "No support for {body.get('resource')} {body.get('event')} notifications." ) data = body.get("data", {}) if data.get("personId") == BOT_ID: logger.info("Ignoring message that we are the sender of.") return HttpResponse(200) context = { "request_scheme": request.scheme, "request_host": request.get_host(), "org_id": body.get("orgId"), "channel_id": data.get("roomId"), "user_id": data.get("personId"), # In a 'attachmentActions' notification, the relevant message ID is 'messageId'. # In a 'messages' notification, the relevant message ID is 'id'. "message_id": data.get("messageId") or data.get("id"), } # In WebEx Teams, the webhook doesn't contain the user/channel/org names. We have to call back for them. # For whatever reason, API.organizations.get() is only permitted by admin users, which the bot is not. # context["org_name"] = API.organizations.get(context["org_id"]).displayName context["channel_name"] = API.rooms.get(context["channel_id"]).title context["user_name"] = API.people.get(context["user_id"]).displayName if body.get("resource") == "messages": # In WebEx Teams, the webhook notification doesn't contain the message text. We have to call back for it. message = API.messages.get(context["message_id"]) command = message.text.strip() # Check for a mention of the bot in the HTML (i.e., if this is not a direct message), and remove it if so. if message.html: bot_mention = re.search( "<spark-mention.*?>(.+?)</spark-mention>", message.html) if bot_mention: command = re.sub(bot_mention.group(1), "", command).strip() command, subcommand, params = parse_command_string(command) elif body.get("resource") == "attachmentActions": # In WebEx Teams, the webhook notification doesn't contain the action details. We have to call back for it. action = API.attachment_actions.get(body.get("data", {}).get("id")) if settings.PLUGINS_CONFIG["nautobot_chatops"].get( "delete_input_on_submission"): # Delete the card that this action was triggered from WebExTeamsDispatcher(context).delete_message( context["message_id"]) if action.inputs.get("action") == "cancel": return HttpResponse(status=200) command, subcommand, params = parse_command_string( action.inputs.get("action")) i = 0 while True: key = f"param_{i}" if key not in action.inputs: break params.append(action.inputs[key]) i += 1 registry = get_commands_registry() if command not in registry: WebExTeamsDispatcher(context).send_markdown(commands_help()) return HttpResponse(status=200) return check_and_enqueue_command(registry, command, subcommand, params, context, WebExTeamsDispatcher)
def post(self, request, *args, **kwargs): """Handle an inbound HTTP POST request representing a user interaction with a UI element.""" valid, reason = verify_signature(request) if not valid: return HttpResponse(status=401, reason=reason) payload = json.loads(request.POST.get("payload", "")) context = { "request_scheme": request.scheme, "request_host": request.get_host(), "org_id": payload.get("team", {}).get("id"), "org_name": payload.get("team", {}).get("domain"), "channel_id": payload.get("channel", {}).get("id"), "channel_name": payload.get("channel", {}).get("name"), "user_id": payload.get("user", {}).get("id"), "user_name": payload.get("user", {}).get("username"), "response_url": payload.get("response_url"), "trigger_id": payload.get("trigger_id"), } # Check for channel_name if channel_id is present if context["channel_name"] is None and context[ "channel_id"] is not None: # Build a Slack Client Object slack_client = WebClient( token=settings.PLUGINS_CONFIG["nautobot_chatops"] ["slack_api_token"]) # Get the channel information from Slack API channel_info = slack_client.conversations_info( channel=context["channel_id"]) # Assign the Channel name out of the conversations info end point context["channel_name"] = channel_info["channel"]["name"] if "actions" in payload and payload["actions"]: # Block action triggered by a non-modal interactive component action = payload["actions"][0] action_id = action.get("action_id", "") block_id = action.get("block_id", "") if action["type"] == "static_select": value = action.get("selected_option", {}).get("value", "") selected_value = f"'{value}'" elif action["type"] == "button": value = action.get("value") selected_value = f"'{value}'" else: logger.error(f"Unhandled action type {action['type']}") return HttpResponse(status=500) if settings.PLUGINS_CONFIG["nautobot_chatops"].get( "delete_input_on_submission"): # Delete the interactive element since it's served its purpose SlackDispatcher(context).delete_message( context["response_url"]) if action_id == "action" and selected_value == "cancel": # Nothing more to do return HttpResponse() elif "view" in payload and payload["view"]: # View submission triggered from a modal dialog logger.info("Submission triggered from a modal dialog") logger.info(json.dumps(payload, indent=2)) values = payload["view"].get("state", {}).get("values", {}) # Handling for multiple fields. This will be used when the multi_input_dialog() method of the Slack # Dispatcher class is utilized. if len(values) > 1: selected_value = "" callback_id = payload["view"].get("callback_id") # sometimes in the case of back-to-back dialogs there will be # parameters included in the callback_id. Below parses those # out and adds them to selected_value. try: cmds = shlex.split(callback_id) except ValueError as e: logger.error("%s", e) return HttpResponse( f"Error: {e} encountered when processing {callback_id}" ) for i, cmd in enumerate(cmds): if i == 2: selected_value += f"'{cmd}'" elif i > 2: selected_value += f" '{cmd}'" action_id = f"{cmds[0]} {cmds[1]}" sorted_params = sorted(values.keys()) for blk_id in sorted_params: for act_id in values[blk_id].values(): if act_id["type"] == "static_select": value = act_id["selected_option"]["value"] selected_value += f" '{value}'" elif act_id["type"] == "plain_text_input": value = act_id["value"] selected_value += f" '{value}'" else: logger.error( f"Unhandled dialog type {act_id['type']}") return HttpResponse(status=500) # Original un-modified single-field handling below else: block_id = sorted(values.keys())[0] action_id = sorted(values[block_id].keys())[0] action = values[block_id][action_id] if action["type"] == "plain_text_input": value = action["value"] selected_value = f"'{value}'" else: logger.error(f"Unhandled action type {action['type']}") return HttpResponse(status=500) # Modal view submissions don't generally contain a channel ID, but we hide one for our convenience: if "private_metadata" in payload["view"]: private_metadata = json.loads( payload["view"]["private_metadata"]) if "channel_id" in private_metadata: context["channel_id"] = private_metadata["channel_id"] else: return HttpResponse("I didn't understand that notification.") logger.info( f"action_id: {action_id}, selected_value: {selected_value}") try: command, subcommand, params = parse_command_string( f"{action_id} {selected_value}") except ValueError as e: logger.error("%s", e) # Tried sending 400 error, but the friendly message never made it to slack. return HttpResponse( f"'Error: {e}' encountered on command '{action_id} {selected_value}'." ) logger.info( f"command: {command}, subcommand: {subcommand}, params: {params}") registry = get_commands_registry() if command not in registry: SlackDispatcher(context).send_markdown( commands_help(prefix=SLASH_PREFIX)) return HttpResponse() # What we'd like to do here is send a "Nautobot is typing..." to the channel, # but unfortunately the API we're using doesn't support that (only the legacy/deprecated RTM API does). # SlackDispatcher(context).send_busy_indicator() return check_and_enqueue_command(registry, command, subcommand, params, context, SlackDispatcher)
def post(self, request, *args, **kwargs): """Handle an inbound HTTP POST request representing a user interaction with a UI element.""" valid, reason = verify_signature(request) if not valid: return HttpResponse(status=401, reason=reason) # For some reason Integration Messages from Mattermost do not show up in POST.items() # in these cases, we have to load the request.body try: data = json.loads(request.body) except ValueError as e: logger.info( "No request body to decode, setting data to empty dict. Error: %s", e) data = {} if request.POST.dict(): data.update(request.POST) context = { "org_id": data.get("team_id"), "org_name": data.get("team_domain"), "channel_id": data.get("channel_id"), "channel_name": data.get("channel_name"), "user_id": data.get("user_id"), "user_name": data.get("user_name"), "response_url": data.get("response_url"), "trigger_id": data.get("trigger_id"), "post_id": data.get("post_id"), "request_scheme": request.get_host(), "request_host": request.get_host(), "integration_url": request.build_absolute_uri( "/api/plugins/chatops/mattermost/interaction/"), } # Check for channel_name if channel_id is present mm_url = settings.PLUGINS_CONFIG["nautobot_chatops"]["mattermost_url"] token = settings.PLUGINS_CONFIG["nautobot_chatops"][ "mattermost_api_token"] if context["channel_name"] is None and context[ "channel_id"] is not None: # Build a Mattermost Client Object mm_client = Driver({ "url": mm_url, "token": token, }) # Get the channel information from Mattermost API channel_info = mm_client.get(f'/channels/{context["channel_id"]}') # Assign the Channel name out of the conversations info end point context["channel_name"] = channel_info["name"] if context["user_name"] is None and context["user_id"] is not None: # Build a Mattermost Client Object mm_client = Driver({ "url": mm_url, "token": token, }) # Get the channel information from Mattermost API user_info = mm_client.get(f'/users/{context["user_id"]}') # Assign the Channel name out of the conversations info end point context["user_name"] = user_info["username"] # Block action triggered by a non-modal interactive component if data.get("context"): action = data.get("context") action_id = action.get("action_id", "") context["token"] = action.get("token", "") if action["type"] == "static_select": value = action.get("selected_option", "") elif action["type"] == "button": value = action.get("value") else: logger.error( f"Unhandled action type {action['type']} in Mattermost Dispatcher" ) return HttpResponse(status=500) selected_value = f"'{value}'" elif data.get("submission"): # View submission triggered from a modal dialog logger.info("Submission triggered from a modal dialog") values = data.get("submission") context["token"] = data.get("state") callback_id = data.get("callback_id") logger.debug(json.dumps(data, indent=2)) # Handling for multiple fields. This will be used when the multi_input_dialog() method of the Mattermost # Dispatcher class is utilized. if len(values) > 1: selected_value = "" # sometimes in the case of back-to-back dialogs there will be # parameters included in the callback_id. Below parses those # out and adds them to selected_value. try: cmds = shlex.split(callback_id) except ValueError as e: logger.error("Mattermost: %s", e) return HttpResponse( status=400, reason= f"Error: {e} encountered when processing {callback_id}" ) for i, cmd in enumerate(cmds): if i == 2: selected_value += f"'{cmd}'" elif i > 2: selected_value += f" '{cmd}'" action_id = f"{cmds[0]} {cmds[1]}" sorted_params = sorted(values.keys()) for blk_id in sorted_params: selected_value += f" '{values[blk_id]}'" # Original un-modified single-field handling below else: action_id = sorted(values.keys())[0] selected_value = values[action_id] else: return HttpResponse( status=500, reason="I didn't understand that notification.") if settings.PLUGINS_CONFIG["nautobot_chatops"].get( "delete_input_on_submission"): # Delete the interactive element since it's served its purpose # Does not work for Ephemeral Posts. if context["post_id"] is not None: MattermostDispatcher(context).delete_message( context["post_id"]) if action_id == "action" and selected_value == "cancel": # Nothing more to do return HttpResponse() logger.info( f"action_id: {action_id}, selected_value: {selected_value}") try: command, subcommand, params = parse_command_string( f"{action_id} {selected_value}") except ValueError as e: logger.error("%s", e) return HttpResponse( status=400, reason= f"Error: {e} encountered on command '{action_id} {selected_value}'" ) logger.info( f"command: {command}, subcommand: {subcommand}, params: {params}") registry = get_commands_registry() if command not in registry: MattermostDispatcher(context).send_markdown(commands_help()) return HttpResponse() MattermostDispatcher(context).send_busy_indicator() return check_and_enqueue_command(registry, command, subcommand, params, context, MattermostDispatcher)
def post(self, request, *args, **kwargs): """Process an inbound HTTP POST request.""" body = json.loads(request.body) valid, reason = verify_jwt_token(request.headers, body) if not valid: return HttpResponse(status=403, reason=reason) if body["type"] not in ["message", "invoke"]: return HttpResponse( status=200, reason=f"No support for {body['type']} notifications") context = { "request_scheme": request.scheme, "request_host": request.get_host(), # We don't get a team_id or a channel_id in direct message conversations "channel_id": body["channelData"].get("channel", {}).get("id"), "org_id": body["channelData"].get("team", {}).get("id"), # Note that the default channel in a team has channel_id == org_id "user_id": body["from"]["id"], "user_name": body["from"]["name"], "user_role": body["from"].get("role"), "conversation_id": body["conversation"]["id"], "conversation_name": body["conversation"].get("name"), "bot_id": body["recipient"]["id"], "bot_name": body["recipient"]["name"], "bot_role": body["recipient"].get("role"), "message_id": body["id"], "service_url": body["serviceUrl"], "tenant_id": body["channelData"]["tenant"]["id"], "is_group": body["conversation"].get("isGroup", False), } if context["org_id"]: # Get the organization name as well response = requests.get( f"{context['service_url']}/v3/teams/{context['org_id']}", headers={ "Authorization": f"Bearer {MSTeamsDispatcher.get_token()}" }, ) response.raise_for_status() context["org_name"] = response.json()["name"] else: # Direct message - use the user as the "organization" - better than nothing context["org_id"] = context["user_id"] context["org_name"] = f"direct message with {context['user_name']}" if context["channel_id"]: # Get the channel name as well response = requests.get( f"{context['service_url']}/v3/teams/{context['org_id']}/conversations", headers={ "Authorization": f"Bearer {MSTeamsDispatcher.get_token()}" }, ) response.raise_for_status() for conversation in response.json()["conversations"]: if conversation["id"] == context["channel_id"]: # The "General" channel has a null name context["channel_name"] = conversation["name"] or "General" break else: # Direct message - use the user as the "channel" - better than nothing context["channel_id"] = context["user_id"] context[ "channel_name"] = f"direct message with {context['user_name']}" if "text" in body: # A command typed directly by the user command = body["text"] # If we get @ed in a channel, the message will be "<at>NAutobot</at> command subcommand" command = re.sub(r"<at>.*</at>", "", command) command, subcommand, params = parse_command_string(command) elif "value" in body: if body["value"].get("type") == "fileUpload": # User either granted or denied permission to upload a file if body["value"]["action"] == "accept": command = body["value"]["context"]["action_id"] context["uploadInfo"] = body["value"]["uploadInfo"] else: command = "cancel" command, subcommand, params = parse_command_string(command) else: # Content that we got from an interactive card command, subcommand, params = parse_command_string( body["value"]["action"]) i = 0 while True: key = f"param_{i}" if key not in body["value"]: break params.append(body["value"][key]) i += 1 if settings.PLUGINS_CONFIG["nautobot_chatops"].get( "delete_input_on_submission"): # Delete the card MSTeamsDispatcher(context).delete_message(body["replyToId"]) if command.startswith("cancel"): # Nothing more to do return HttpResponse(status=200) else: command = "" subcommand = "" params = [] registry = get_commands_registry() if command not in registry: MSTeamsDispatcher(context).send_markdown(commands_help()) return HttpResponse(status=200) # Send "typing" indicator to the client so they know we received the request MSTeamsDispatcher(context).send_busy_indicator() return check_and_enqueue_command(registry, command, subcommand, params, context, MSTeamsDispatcher)