Skip to content

Commit a44506f

Browse files
authored
Merge pull request #138 from TeskaLabs/feature/slack-refactoring-at2606
Refactoring a Slack provider.
2 parents 72882a7 + 7280c62 commit a44506f

6 files changed

Lines changed: 121 additions & 106 deletions

File tree

asabiris/app.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,7 @@ def __init__(self, args=None):
123123
else:
124124
# Initialize the SlackOutputService
125125
self.SlackOutputService = SlackOutputService(self)
126-
127-
# Only initialize SendSlackOrchestrator if the SlackOutputService client is valid
128-
if self.SlackOutputService.Client is None:
129-
# If client is None, disable Slack orchestrator as well
130-
self.SendSlackOrchestrator = None
131-
else:
132-
# If the client is valid, initialize the orchestrator
133-
self.SendSlackOrchestrator = SendSlackOrchestrator(self)
126+
self.SendSlackOrchestrator = SendSlackOrchestrator(self)
134127

135128
else:
136129
# If the slack section is not present in the config, set both services to None

asabiris/orchestration/sendslack.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ async def send_to_slack(self, msg):
3838

3939
body = msg['body']
4040
template = body["template"]
41+
channel = body.get("channel", None)
42+
43+
# This allows to speficy channel id or member id directly, skipping the lookup by channel name.
44+
channel_id = body.get("channel_id", None)
45+
if channel_id is not None:
46+
channel = "id " + channel_id
47+
4148
attachments = msg.get("attachments", None)
4249
# if params no provided pass empty params
4350
# - primarily use absolute path - starts with "/"
@@ -81,14 +88,14 @@ async def send_to_slack(self, msg):
8188
fallback_message = output
8289
blocks = None
8390

84-
await self.SlackOutputService.send_message(blocks, fallback_message)
91+
await self.SlackOutputService.send_message(blocks, fallback_message, channel)
8592
return
8693

8794
# Sending attachments
8895

8996
output = self.MarkdownFormatterService.unformat(output)
9097
atts_gen = self.AttachmentRenderingService.render_attachment(attachments)
91-
await self.SlackOutputService.send_files(output, atts_gen)
98+
await self.SlackOutputService.send_files(output, atts_gen, channel)
9299

93100

94101
async def render_attachment(self, template, params):

asabiris/output/slack/service.py

Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1+
import time
12
import logging
23
import configparser
4+
5+
import asab
6+
37
try:
4-
import slack_sdk.errors as slack_errors
5-
from slack_sdk import WebClient
6-
SlackApiError = slack_errors.SlackApiError
8+
import slack_sdk
9+
import slack_sdk.errors
710
except ModuleNotFoundError:
8-
slack_errors = None
9-
WebClient = None
11+
slack_sdk = None
1012

11-
class SlackApiError(Exception):
12-
pass
1313
from ...errors import ASABIrisError, ErrorCode
14-
15-
import asab
16-
1714
from ...output_abc import OutputABC
1815

16+
if slack_sdk is not None:
17+
SlackApiError = slack_sdk.errors.SlackApiError
18+
else:
19+
SlackApiError = Exception
20+
1921

2022
L = logging.getLogger(__name__)
2123

@@ -30,62 +32,87 @@ def check_config(config, section, parameter):
3032

3133

3234
class SlackOutputService(asab.Service, OutputABC):
35+
3336
def __init__(self, app, service_name="SlackOutputService"):
3437
super().__init__(app, service_name)
38+
self.ConfigService = app.get_service("TenantConfigExtractionService")
3539

3640
# Load global configuration as defaults
37-
self.SlackWebhookUrl = check_config(asab.Config, "slack", "token")
38-
self.Channel = check_config(asab.Config, "slack", "channel")
39-
# Keep tenant-config service always available
40-
self.ConfigService = app.get_service("TenantConfigExtractionService")
41+
self.ConfigToken = check_config(asab.Config, "slack", "token")
42+
self.ConfigChannel = check_config(asab.Config, "slack", "channel")
4143

42-
# If required Slack configuration is missing, disable Slack service
43-
if not self.SlackWebhookUrl or not self.Channel:
44-
L.warning("Slack output service is not properly configured. Disabling Slack service.")
45-
self.Client = None
46-
return
44+
self.Cache = {}
4745

48-
if WebClient is None:
49-
L.warning("slack_sdk is not installed. Slack service is disabled.")
50-
self.Client = None
46+
if slack_sdk is None:
47+
L.warning("slack_sdk library is not installed. Slack service is disabled.")
5148
return
5249

53-
self.Client = WebClient(token=self.SlackWebhookUrl)
50+
app.PubSub.subscribe("Application.tick/1800!", self._on_tick)
5451

5552

56-
async def send_message(self, blocks, fallback_message) -> None:
57-
"""
58-
Sends a message to a Slack channel.
59-
"""
60-
if self.Client is None:
61-
L.warning("SlackOutputService is not initialized properly. Message will not be sent.")
62-
return
53+
def _on_tick(self, event):
54+
# clear cache every 1800 seconds
55+
to_delete = []
56+
for key, value in self.Cache.items():
57+
if time.time() - value[2] > 3600:
58+
to_delete.append(key)
59+
for key in to_delete:
60+
self.Cache.pop(key, None)
6361

62+
63+
def _resolve(self, channel=None):
6464
try:
6565
effective_tenant = asab.contextvars.Tenant.get()
6666
except LookupError:
6767
effective_tenant = None
6868

6969
# determine which token/channel to use
70-
token, channel = (self.SlackWebhookUrl, self.Channel)
7170
if effective_tenant and self.ConfigService is not None:
7271
try:
73-
token, channel = self.ConfigService.get_slack_config(effective_tenant)
72+
token, default_channel = self.ConfigService.get_slack_config(effective_tenant)
7473
except KeyError:
7574
L.warning(
7675
"Tenant-specific Slack configuration not found for '%s'. Using global config.",
7776
effective_tenant
7877
)
78+
token, default_channel = self.ConfigToken, self.ConfigChannel
79+
else:
80+
token, default_channel = self.ConfigToken, self.ConfigChannel
81+
82+
if channel is None:
83+
channel = default_channel
84+
85+
cache_hit = self.Cache.get((token, channel), None)
86+
if cache_hit is not None:
87+
return cache_hit[0], cache_hit[1]
88+
89+
client = slack_sdk.WebClient(token=token)
90+
channel_id = self.get_channel_id(client, channel)
91+
92+
self.Cache[(token, channel)] = (client, channel_id, time.time())
93+
94+
return client, channel_id
95+
96+
97+
async def send_message(self, blocks, fallback_message, channel=None) -> None:
98+
"""
99+
Sends a message to a Slack channel.
100+
"""
101+
if slack_sdk is None:
102+
L.warning("slack_sdk library is not installed. Slack service is disabled.")
103+
return
104+
105+
client, channel_id = self._resolve(channel)
79106

80107
if channel is None:
81108
raise ValueError("Cannot send message to Slack. Reason: Missing Slack channel")
82-
if token is None:
83-
raise ValueError("Cannot send message to Slack. Reason: Missing Webhook URL or token")
109+
if client is None:
110+
raise ValueError("Cannot send message to Slack.")
84111

85112
# Audit log of outgoing payload at NOTICE level
86113
L.log(
87114
asab.LOG_NOTICE,
88-
"SlackOutputService.send_message → channel=%s, text=%r, blocks=%r",
115+
"SlackOutputService.send_message",
89116
struct_data={
90117
"channel": channel,
91118
"text": fallback_message,
@@ -94,8 +121,6 @@ async def send_message(self, blocks, fallback_message) -> None:
94121
)
95122

96123
try:
97-
client = WebClient(token=token)
98-
channel_id = self.get_channel_id(client, channel)
99124
client.chat_postMessage(
100125
channel=channel_id,
101126
text=fallback_message,
@@ -117,33 +142,15 @@ async def send_message(self, blocks, fallback_message) -> None:
117142
)
118143

119144

120-
async def send_files(self, body: str, atts_gen,):
145+
async def send_files(self, body: str, atts_gen, channel=None):
121146
"""
122147
Sends a message to a Slack channel with attachments.
123148
"""
124-
if self.Client is None:
125-
L.warning("SlackOutputService is not initialized properly. File will not be sent.")
149+
if slack_sdk is None:
150+
L.warning("slack_sdk library is not installed. Slack service is disabled.")
126151
return
127152

128-
try:
129-
effective_tenant = asab.contextvars.Tenant.get()
130-
except LookupError:
131-
effective_tenant = None
132-
133-
token, channel = (self.SlackWebhookUrl, self.Channel)
134-
if effective_tenant and self.ConfigService is not None:
135-
try:
136-
token, channel = self.ConfigService.get_slack_config(effective_tenant)
137-
except KeyError:
138-
L.warning("Tenant-specific Slack configuration not found for '{}'. Using global config.".format(effective_tenant))
139-
140-
if channel is None:
141-
raise ValueError("Cannot send message to Slack. Reason: Missing Slack channel")
142-
if token is None:
143-
raise ValueError("Cannot send message to Slack. Reason: Missing Webhook URL or token")
144-
145-
client = WebClient(token=token)
146-
channel_id = self.get_channel_id(client, channel)
153+
client, channel_id = self._resolve(channel)
147154

148155
try:
149156
async for attachment in atts_gen:
@@ -185,13 +192,19 @@ async def send_files(self, body: str, atts_gen,):
185192
)
186193

187194

188-
def get_channel_id(self, client, channel_name, types="public_channel"):
195+
def get_channel_id(self, client, channel_name, types=None):
189196
"""
190197
Fetches Slack channel ID from Slack API.
191198
"""
199+
if types is None:
200+
types = ["public_channel", "private_channel"]
201+
202+
if channel_name.startswith("id "):
203+
return channel_name.split("id ")[1]
204+
192205
for response in client.conversations_list(types=types):
193206
for channel in response['channels']:
194-
if channel['name'] == channel_name:
207+
if channel.get('name') == channel_name:
195208
return channel['id']
196209

197210
# Business-level error: channel not found

docs/bruno/Send Slack.bru

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
meta {
2+
name: Send Slack
3+
type: http
4+
seq: 4
5+
}
6+
7+
put {
8+
url: {{BASE_URL}}/send_slack
9+
body: json
10+
auth: inherit
11+
}
12+
13+
headers {
14+
Content-Type: application/json
15+
}
16+
17+
body:json {
18+
{
19+
"type": "slack",
20+
"body": {
21+
"template": "/Templates/Slack/message.md",
22+
"channel_id": "U0124ABCD",
23+
"params": {
24+
"content": "This is a Slack message ;-)"
25+
}
26+
}
27+
}
28+
29+
30+
}

library/Templates/Slack/example.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*📧 Slack message from asab-iris 📧*
2+
3+
This is the example of the Slack message sent by `asab-iris`.
4+
{% if name %}*Sent by:* {{ name }}{% endif %}
5+
6+
More at <https://github.com/TeskaLabs/asab-iris|github.com/TeskaLabs/asab-iris>

library/Templates/Slack/message.md

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1 @@
1-
*🚨 Alert Management Notification 🚨*
2-
3-
*<{{ public_url }}/?tenant={{ tenant }}#|TeskaLabs LogMan.io>* has identified a noteworthy event in your IT infrastructure that might require your immediate attention. 🕵️‍♂️ Please review the following summary of the event:
4-
5-
{% if ticketid %}
6-
🔖 *Ticket ID:* {{ ticketid }} - This ticket has been updated.
7-
{% endif %}
8-
9-
{% if type %}
10-
📑 *Type of Ticket:* {{ type }}
11-
{% endif %}
12-
13-
{% if status %}
14-
📊 *Status Update:* The status was changed to: *{{ status }}*
15-
{% endif %}
16-
17-
{% if credid %}
18-
🔑 *Changed by Credential ID:* {{ credid }}
19-
{% endif %}
20-
21-
{% if username %}
22-
👤 *Changed by User:* {{ username }}
23-
{% endif %}
24-
25-
We encourage you to *review this incident promptly* to determine the next appropriate course of action.
26-
27-
🔍 To examine the incident in greater detail, please log in to your *<{{ public_url }}/?tenant={{ tenant }}#|TeskaLabs LogMan.io>*.
28-
29-
💡 *Remember,* the effectiveness of any security program lies in a swift response. Thank you for your attention to this matter.
30-
31-
Stay safe,
32-
33-
*<{{ public_url }}/?tenant={{ tenant }}#|TeskaLabs LogMan.io>*
34-
35-
Made with ❤️ by *<https://teskalabs.com|TeskaLabs>*
1+
{% if content %}{{ content }}{% endif %}

0 commit comments

Comments
 (0)