1+ import time
12import logging
23import configparser
4+
5+ import asab
6+
37try :
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
710except ModuleNotFoundError :
8- slack_errors = None
9- WebClient = None
11+ slack_sdk = None
1012
11- class SlackApiError (Exception ):
12- pass
1313from ...errors import ASABIrisError , ErrorCode
14-
15- import asab
16-
1714from ...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
2022L = logging .getLogger (__name__ )
2123
@@ -30,62 +32,87 @@ def check_config(config, section, parameter):
3032
3133
3234class 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
0 commit comments