def replace_message(channel, message_id, text=None, content=None): """ Replace an existing message in Slack. The message will need to have been published by the bot. The `text` parameter is not required when the `content` parameter is provided, however including it is still highly recommended. :param channel: The identifier of the Slack conversation the message was posted to :param message_id: The timestamp of the message to be updated :param text: Message text (Formatting: https://api.slack.com/reference/surfaces/formatting) :param content: List of valid blocks data (https://api.slack.com/block-kit) :return: Response object (Dictionary) """ if not settings.SLACK_TOKEN: return {'ok': False, 'error': 'config_error'} client = WebClient(token=settings.SLACK_TOKEN) if content or text: try: response = client.chat_update(channel=channel, ts=message_id, as_user=True, text=text, blocks=content, link_names=True) assert response['ok'] is True return {'ok': True, 'message': response['message']} except SlackApiError as e: assert e.response['ok'] is False return e.response else: return {'ok': False, 'error': 'no_text'}
class Slack: def __init__(self): load_dotenv() self.message_template = { "info": { "emoji": ":large_blue_circle:", "header": "Info", }, "danger": { "emoji": ":red_circle:", "header": "Error", } } self.channel = 'C011C9E9RC7' self.client = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) self.last_ts = None def post(self, message, mtype): blocks = self.generate_blocks(message, mtype) response = self.client.chat_postMessage(channel=self.channel, blocks=blocks) self.set_ts(response) def update(self, message, mtype): blocks = self.generate_blocks(message, mtype) response = self.client.chat_update(channel=self.channel, ts=self.last_ts, blocks=blocks) self.set_ts(response) def set_ts(self, resp): self.last_ts = resp['ts'] def generate_blocks(self, message, mtype): blocks = [] blocks.append({ "type": "section", "text": { "type": "mrkdwn", "text": f"{self.message_template[mtype]['emoji']}*{self.message_template[mtype]['header']}*: {message}" } }) return blocks
class SlackApp: # Slack client for Web API requests client = WebClient() SLACK_BOT_TOKEN = None SLACK_VERIFICATION_TOKEN = None SLACK_SIGNING_SECRET = None SLACK_CHANNEL_ID = None BUCKET_PATH = None def __init__(self, SLACK_BOT_TOKEN, SLACK_VERIFICATION_TOKEN, SLACK_SIGNING_SECRET, SLACK_CHANNEL_ID, BUCKET_PATH): self.SLACK_BOT_TOKEN = SLACK_BOT_TOKEN self.SLACK_VERIFICATION_TOKEN = SLACK_VERIFICATION_TOKEN self.SLACK_SIGNING_SECRET = SLACK_SIGNING_SECRET self.SLACK_CHANNEL_ID = SLACK_CHANNEL_ID self.BUCKET_PATH = BUCKET_PATH self.client = WebClient(self.SLACK_BOT_TOKEN) def list_goals(self): request_json_py = {"active": "true"} request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) goals_response = GetGoals(request_json) goals_json = goals_response.get_json() var_today = datetime.now(pytz.timezone("Africa/Johannesburg")) var_today_str = var_today.strftime("%Y-%m-%d") parent_message = self.client.chat_postMessage( channel=self.SLACK_CHANNEL_ID, blocks=[ SlackComposer.MessageBlock_Divider(), SlackComposer.MessageBlock_TextHeader( ":dart: You have {} goals ({})".format( len(goals_json), var_today_str)) ]) parent_ts = parent_message["ts"] for index in goals_json: self.client.chat_postMessage(channel=self.SLACK_CHANNEL_ID, blocks=SlackComposer.Attachment_Goal( goals_json[index]), reply_broadcast="false", thread_ts=parent_ts) return make_response("", 200) def list_transactions(self): request_json_py = {"only_uncategorised": "True"} request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) transactions_response = GetTransactions(request_json) transactions_json = transactions_response.get_json() var_today = datetime.now(pytz.timezone("Africa/Johannesburg")) var_today_str = var_today.strftime("%Y-%m-%d") parent_message = self.client.chat_postMessage( channel=self.SLACK_CHANNEL_ID, blocks=[ SlackComposer.MessageBlock_Divider(), SlackComposer.MessageBlock_TextHeader( ":construction: You have {} uncategorised transactions ({})" .format(len(transactions_json), var_today_str)) ]) parent_ts = parent_message["ts"] for index in transactions_json: self.client.chat_postMessage( channel=self.SLACK_CHANNEL_ID, attachments=SlackComposer.Attachment_Transaction( transactions_json[index]), reply_broadcast="false", thread_ts=parent_ts) return make_response("", 200) def post_transactions_to_channel(self, transaction_ids): request_json = json.loads(transaction_ids) transactions_response = GetTransactions(request_json) transactions_json = transactions_response.get_json() for index in transactions_json: self.client.chat_postMessage( channel=self.SLACK_CHANNEL_ID, attachments=SlackComposer.Attachment_Transaction( transactions_json[index]), reply_broadcast="false") return make_response("", 200) def categorise_transaction(self, transaction_uuid, category_id, message_ts): # Get transaction request_json_py = {"doc_id": transaction_uuid} request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) response = GetTransactions(request_json) transaction_json = response.get_json() transaction_json["0"]["budget_category"] = category_id # Update transaction UpdateTransaction(transaction_json["0"], transaction_uuid) # Get transaction request_json_py = {"doc_id": transaction_uuid} request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) response = GetTransactions(request_json) transaction_json = response.get_json() # Update transaction on Slack self.client.chat_update( channel=self.SLACK_CHANNEL_ID, ts=message_ts, attachments=SlackComposer.Attachment_Transaction( transaction_json["0"]), ) return make_response("", 200) def delete_goal(self, goal_id, message_ts): request_json_py = {"doc_ids": [goal_id]} request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) DeleteGoals(request_json) self.client.chat_update( channel=self.SLACK_CHANNEL_ID, ts=message_ts, blocks=[{ "type": "section", "text": { "type": "mrkdwn", "text": "*DELETED:* ~{}~".format(goal_id), }, }], ) return make_response("", 200) def refresh_goal_modal(self, current_goal_data, view_id, goal_id, message_ts, title, description): # request_json_py = {"active": "true", "doc_id": goal_id} # request_json_str = json.dumps(request_json_py) # request_json = json.loads(request_json_str) # goals_response = GetGoals(request_json) # goals_json = goals_response.get_json() goal = Goal() goal.setup_goal(current_goal_data) view = SlackComposer.Modal_Goal(title, description, goal, goal_id, message_ts) self.client.views_update(view=view, view_id=view_id) return make_response("", 200) def new_goal_modal(self, trigger_id): view = SlackComposer.Modal_Goal( "New Goal", "The following form will help you to create a new goal.") self.client.views_open(trigger_id=trigger_id, view=view) return make_response("", 200) def new_goal(self, request_json_py): request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) AddGoal(request_json) return make_response("", 200) def update_goal_modal(self, goal_id, trigger_id, message_ts): request_json_py = {"active": "true", "doc_id": goal_id} request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) goals_response = GetGoals(request_json) goals_json = goals_response.get_json() goal = Goal() # goal_doc_id = goals_json["0"]["doc_id"] goal.setup_goal(goals_json["0"]) view = SlackComposer.Modal_Goal( "Update Goal", "The following form will help you to update a goal.", goal, goal_id, message_ts) self.client.views_open(trigger_id=trigger_id, view=view) return make_response("", 200) def update_goal(self, request_json_py, goal_uuid, message_ts): request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) UpdateGoal(request_json, goal_uuid) # Get goal request_json_py = {"doc_id": goal_uuid} request_json_str = json.dumps(request_json_py) request_json = json.loads(request_json_str) response = GetGoals(request_json) goal_json = response.get_json() # Update transaction on Slack self.client.chat_update( channel=self.SLACK_CHANNEL_ID, ts=message_ts, blocks=SlackComposer.Attachment_Goal(goal_json["0"]), ) return make_response("", 200) def convert_view_to_data(self, payload): state_values = payload["view"]["state"]["values"] input_type = state_values[SlackComposer.inputId_type][ SlackComposer.inputId_type]["selected_option"]["value"] input_spendingType = state_values[SlackComposer.inputId_spendingType][ SlackComposer.inputId_spendingType]["selected_option"]["value"] input_start = state_values[SlackComposer.inputId_start][ SlackComposer.inputId_start]["selected_date"] input_end = state_values[SlackComposer.inputId_end][ SlackComposer.inputId_end]["selected_date"] input_count_limit = None input_value_limit = None if SlackComposer.inputId_count_limit in state_values: if state_values[SlackComposer.inputId_count_limit][ SlackComposer.inputId_count_limit]["value"] is not None: input_count_limit = int( state_values[SlackComposer.inputId_count_limit][ SlackComposer.inputId_count_limit]["value"]) if SlackComposer.inputId_value_limit in state_values: if state_values[SlackComposer.inputId_value_limit][ SlackComposer.inputId_value_limit]["value"] is not None: input_value_limit = int( state_values[SlackComposer.inputId_value_limit][ SlackComposer.inputId_value_limit]["value"]) input_category = BudgetCategory.Default.value input_merchant_based = False input_merchant_name = None input_merchant_code = None if (len(state_values[SlackComposer.inputId_merchant_based][ SlackComposer.inputId_merchant_based]["selected_options"]) > 0): input_merchant_based = bool( state_values[SlackComposer.inputId_merchant_based][ SlackComposer.inputId_merchant_based]["selected_options"] [0]["value"]) if (SlackComposer.inputId_merchant_name in state_values): input_merchant_name = state_values[ SlackComposer.inputId_merchant_name][ SlackComposer.inputId_merchant_name]["value"] if (SlackComposer.inputId_merchant_code in state_values): input_merchant_code = state_values[ SlackComposer.inputId_merchant_code][ SlackComposer.inputId_merchant_code]["value"] if (SlackComposer.inputId_category in state_values): input_category = state_values[SlackComposer.inputId_category][ SlackComposer.inputId_category]["selected_option"]["value"] # if (SlackComposer.inputId_count_limit in state_values): # input_count_limit = state_values[SlackComposer.inputId_count_limit][SlackComposer.inputId_count_limit]["value"] # if (SlackComposer.inputId_value_limit in state_values): # input_value_limit = state_values[SlackComposer.inputId_value_limit][SlackComposer.inputId_value_limit]["value"] request_json_py = { "goal_type": "{}".format(input_type), "start_date": "{}".format(input_start), "end_date": "{}".format(input_end), "active": True, "goal_details": { "spending_type": "{}".format(input_spendingType), "merchant_based": input_merchant_based, # "merchant": { # "merchant": "{}".format(input_merchant_name), # "merchant_code": "{}".format(input_merchant_code), # }, # "value_limit": "{}".format(input_value_limit), # "count_limit": "{}".format(input_count_limit), "budget_category": { "category": "{}".format(input_category) }, }, } if input_merchant_based is True: request_json_py["goal_details"]["merchant"] = { "merchant": "{}".format(input_merchant_name), "merchant_code": "{}".format(input_merchant_code), } if input_value_limit is not None: request_json_py["goal_details"]["value_limit"] = input_value_limit if input_count_limit is not None: request_json_py["goal_details"]["count_limit"] = input_count_limit return request_json_py def Avatar_SendToSlack(self, payload): # Switch to different slack messages if payload["type"] == "avatar_update": return self.AvatarUpdate_FormatMessage(payload["content"]) elif payload["type"] == "avatar_report": # Load avatar date from DB data = AvatarReport_PrepareData() return self.AvatarUpdate_FormatMessage(data) else: print("failed - unknown payload type") return def AvatarUpdate_FormatMessage(self, data): # Only send avatar image if level changed or if report type imageUrl = None level = None if data["level_diff"] != 0 or "report" in data: if self.BUCKET_PATH is False: return level = str(data["level"]) imageUrl = self.BUCKET_PATH.replace("{REPLACE_LEVEL}", level) # imageBlock = SlackComposer.MessageBlock_Image(imageUrl, level) # Load header and stats header = SlackComposer.AvatarUpdate_GetHeader(data) stats = SlackComposer.AvatarUpdate_GetStats(data) message_blocks = [ SlackComposer.MessageBlock_Divider(), SlackComposer.MessageBlock_TextHeader(header), SlackComposer.MessageBlock_Text(stats) ] if imageUrl is not None: message_blocks.append( SlackComposer.MessageBlock_Image(imageUrl, level)) parent_message = self.client.chat_postMessage( channel=self.SLACK_CHANNEL_ID, blocks=message_blocks) return make_response("", parent_message.status_code)
def test_missing_text_warnings_chat_update(self): client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") resp = client.chat_update(channel="C111", ts="111.222", blocks=[]) self.assertIsNone(resp["error"])
class Slack: def __init__(self, token, channel_name=None, channel_id=None): self.client = WebClient(token) self.channel_id = channel_id channels = self.client.conversations_list( types='public_channel,private_channel') if channel_name: for channel in channels['channels']: if channel['name'] == channel_name: self.channel_id = channel['id'] break if not self.channel_id: self.channel_id = self.client.conversations_create( name=channel_name.lower(), is_private=True)['channel']['id'] admins = [ u['id'] for u in self.client.users_list()['members'] if u.get('is_admin') or u.get('is_owner') ] self.client.conversations_invite(channel=self.channel_id, users=admins) def send_snippet(self, title, initial_comment, code, code_type='python', thread_ts=None): return self.client.files_upload( channels=self.channel_id, title=title, initial_comment=initial_comment.replace('<br>', ''), content=code, filetype=code_type, thread_ts=thread_ts)['ts'] def send_exception_snippet(self, domain, event, code_type='python', thread_ts=None): message = traceback.format_exc() + '\n\n\n' + dumps(event, indent=2) subject = 'Error occurred in ' + domain self.send_snippet(subject, subject, message, code_type=code_type, thread_ts=thread_ts) def send_raw_message(self, blocks, thread_ts=None): return self.client.chat_postMessage(channel=self.channel_id, blocks=blocks, thread_ts=thread_ts)['ts'] def update_raw_message(self, ts, blocks): self.client.chat_update(channel=self.channel_id, blocks=blocks, ts=ts) def get_perm_link(self, ts): return self.client.chat_getPermalink(channel=self.channel_id, message_ts=ts)['permalink'] def send_message(self, message, attachment=None, thread_ts=None): blocks = [{ 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': message.replace('<br>', '') } }, { 'type': 'divider' }] if attachment: blocks[0]['accessory'] = { 'type': 'button', 'text': { 'type': 'plain_text', 'text': attachment['text'], 'emoji': True }, 'url': attachment['value'] } return self.send_raw_message(blocks, thread_ts)
class SlackAction: client = None channel = None name = None details = None redis = None logger = None type = None def __init__(self, attack_details=None, update_message=None, redis=None): self.client = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) self.channel = os.environ["SLACK_BOT_CHANNEL"] self.name = os.getenv("SLACK_BOT_NAME", "FastNetMon") if attack_details: self.details = attack_details["details"] self.type = "attack" elif update_message: self.details = update_message self.type = "update" self.redis = redis self.logger = logging.getLogger(__name__) def _get_message_payload( self, message, mitigation_rules=None, packet_details=None, attack_details=None, thread_ts=None, fallback_message=None, actions=None, ): attachments = [] blocks = message if actions: blocks = blocks + actions if attack_details is not None: attachments.append(attack_details) if mitigation_rules is not None: attachments = attachments + mitigation_rules if packet_details is not None: attachments.append(packet_details) return { "channel": self.channel, "username": self.name, "thread_ts": thread_ts, "icon_emoji": ":robot_face:", "fallback": fallback_message, "blocks": blocks, "attachments": attachments, } def _notify(self, message): try: # logger.warning(json.dumps(message, indent=4)) attachments = message["attachments"] del message["attachments"] response = self.client.chat_postMessage(**message) assert response["message"] if message["thread_ts"] is None: message_thread_id = response["ts"] else: message_thread_id = message["thread_ts"] time.sleep(1) del message["blocks"] for attachment in attachments: message["attachments"] = [attachment] message["thread_ts"] = message_thread_id response = self.client.chat_postMessage(**message) assert response["message"] time.sleep(1) return message_thread_id except SlackApiError as e: # You will get a SlackApiError if "ok" is False assert e.response["ok"] is False assert e.response[ "error"] # str like 'invalid_auth', 'channel_not_found' self.logger.warning(f"Got an error: {e.response['error']}") self.logger.warning(json.dumps(message, indent=4)) def _build_attack_details_table(self): dataset = self.details["attack_details"] dataset = dict(sorted(dataset.items())) attack_summary_fields = [] for field in dataset: if "traffic" in field: raw_value = self.details["attack_details"][field] value = format_bps(raw_value) else: value = str(self.details["attack_details"][field]) if value == "": value = "<not set>" attack_summary_fields.append("_{field}:_ {value}".format( field=field, value=value)) return "\n".join(attack_summary_fields) def _build_flowspec_details_table(self, rule): flowspec_details = [] for field in rule: value = rule[field] if isinstance(value, list): value = ", ".join(str(x) for x in value) if value == "": value = "<not set>" flowspec_details.append("_{field}:_ {value}".format(field=field, value=value)) return "\n".join(flowspec_details) def _get_attack_details(self): fields = self._build_attack_details_table() return { "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "*Attack Summary Details*" }, }, { "type": "section", "text": { "type": "mrkdwn", "text": fields } }, ], "fallback": "Summary of attack volumetric data", } def _get_flowspec_blocks(self, rule): fields = self._build_flowspec_details_table(rule) return [ { "type": "section", "text": { "type": "mrkdwn", "text": "*Flowspec Rules*" }, }, { "type": "section", "text": { "type": "mrkdwn", "text": fields } }, ] def process_message(self): if self.type == "attack": self.process_attack_message() elif self.type == "update": self.process_update_message() def process_update_message(self): if "message" in self.details: blocks = self.details["message"]["blocks"] new_blocks = [] for block in blocks: if block["type"] != "actions": new_blocks.append(block) try: # logger.warning(json.dumps(message, indent=4)) response = self.client.chat_update( channel=self.details["channel"]["id"], ts=self.details["message"]["ts"], text="Ban has been removed", blocks=new_blocks, ) assert response["message"] return response["ts"] except SlackApiError as e: # You will get a SlackApiError if "ok" is False assert e.response["ok"] is False assert e.response[ "error"] # str like 'invalid_auth', 'channel_not_found' self.logger.warning(f"Got an error: {e.response['error']}") self.logger.warning(json.dumps(self.details, indent=4)) def process_attack_message(self): if self.details["action"] == "ban" or self.details[ "action"] == "partial_block": attack_description = ( "*RTBH IP {ip_address}*: {attack_protocol} " + "{attack_direction} with {attack_severity} severity {attack_type} " + "attack").format( ip_address=self.details["ip"], attack_protocol=self.details["attack_details"] ["attack_protocol"], attack_direction=self.details["attack_details"] ["attack_direction"], attack_severity=self.details["attack_details"] ["attack_severity"], attack_type=self.details["attack_details"]["attack_type"], ) flowspec_attachments = None redis_key = self.details["attack_details"]["attack_uuid"] if self.details["action"] == "partial_block": redis_key = "fs-{attack_direction}-{ip_address}".format( attack_direction=self.details["attack_details"] ["attack_direction"], ip_address=self.details["ip"], ) attack_description = ( "*Flow Mitigation for IP {ip_address}*: {attack_protocol} " + "{attack_direction} with {attack_severity} severity {attack_type} " + "attack" ).format( ip_address=self.details["ip"], attack_protocol=self.details["attack_details"] ["attack_protocol"], attack_direction=self.details["attack_details"] ["attack_direction"], attack_severity=self.details["attack_details"] ["attack_severity"], attack_type=self.details["attack_details"]["attack_type"], ) flowspec_attachments = [] for rule in self.details["flow_spec_rules"]: flowspec_details = { "blocks": [{ "type": "section", "text": { "type": "mrkdwn", "text": "*Flow Rules for the block*", }, }], "fallback": "Flow rules for the attack", } flowspec_details["blocks"] = flowspec_details[ "blocks"] + self._get_flowspec_blocks(rule) flowspec_attachments.append(flowspec_details) packet_capture_details = "Not available" if "packet_dump" in self.details: packet_capture_details = "```{packet_details}```".format( packet_details="\n".join(self.details["packet_dump"])) attack_summary_block = [ { "type": "section", "text": { "type": "mrkdwn", "text": attack_description }, }, { "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", "text": "Violation reason is {violation} in {direction} direction" .format( violation=self.details["attack_details"] ["attack_detection_threshold"], direction=self.details["attack_details"] ["attack_direction"], ), }, }, ] attack_data = self._get_attack_details() packet_details = { "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "*Packet Capture Sample*" }, }, { "type": "section", "text": { "type": "mrkdwn", "text": packet_capture_details }, }, ], "fallback": "Packets in the capture", } actions = [{ "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Remove block :lock:", "emoji": True, }, "value": self.details["attack_details"]["attack_uuid"], }, ], }] message_thread = self.redis.get(redis_key) if message_thread is not None: message_thread = message_thread.decode("utf-8") actions = None message = self._get_message_payload( message=attack_summary_block, attack_details=attack_data, packet_details=packet_details, mitigation_rules=flowspec_attachments, fallback_message=attack_description, actions=actions, thread_ts=message_thread, ) message_thread = self._notify(message) self.redis.set(redis_key, message_thread) if self.details["action"] == "partial_block": self.redis.expire(redis_key, 1800) elif self.details["action"] == "unban": ban_id = self.details["attack_details"]["attack_uuid"] message_thread = self.redis.get(ban_id) if message_thread is not None: message_thread = message_thread.decode("utf-8") tz = timezone(os.getenv("TIMEZONE", "Australia/Sydney")) action_time = (datetime.utcnow().replace( tzinfo=timezone("utc")).astimezone(tz=tz)) action_description = [{ "type": "section", "text": { "type": "mrkdwn", "text": "*Ban removed* for {ip_address} at {datetime}".format( ip_address=self.details["ip"], datetime=action_time.strftime( "%a %b %d %H:%M:%S %Z %Y"), ), }, }] message = self._get_message_payload( message=action_description, thread_ts=message_thread, fallback_message="Ban removed", ) self._notify(message) else: self.logger.warn("Data for unknown action type {action}".format( action=self.details["action"]))