77from .base_command import BaseCommand
88from ..models import MeshMessage
99import asyncio
10+ import re
1011
1112
1213class ChannelsCommand (BaseCommand ):
@@ -47,7 +48,6 @@ def matches_keyword(self, message: MeshMessage) -> bool:
4748 return True
4849
4950 # Check for word boundary matches using regex
50- import re
5151 # Create a regex pattern that matches the keyword at word boundaries
5252 # Use custom word boundary that treats underscores as separators
5353 # (?<![a-zA-Z0-9]) = negative lookbehind for alphanumeric characters (not underscore)
@@ -85,12 +85,17 @@ async def execute(self, message: MeshMessage) -> bool:
8585 specific_channel = sub_command
8686 sub_command = None
8787 else :
88- # Check if this might be a channel search (not a category)
89- # Try to find a channel that matches this name across all categories
90- found_channel = self ._find_channel_by_name (sub_command )
91- if found_channel :
92- specific_channel = '#' + found_channel
93- sub_command = None
88+ # First check if this is a valid category
89+ if self ._is_valid_category (sub_command ):
90+ # It's a category, keep it as sub_command
91+ pass
92+ else :
93+ # Check if this might be a channel search (not a category)
94+ # Try to find a channel that matches this name across all categories
95+ found_channel = self ._find_channel_by_name (sub_command )
96+ if found_channel :
97+ specific_channel = '#' + found_channel
98+ sub_command = None
9499
95100 # Handle specific channel request
96101 if specific_channel :
@@ -116,12 +121,7 @@ async def execute(self, message: MeshMessage) -> bool:
116121 messages = self ._split_into_messages (channel_list , sub_command )
117122
118123 # Send each message with a small delay between them
119- for i , msg_content in enumerate (messages ):
120- if i > 0 :
121- # Small delay between messages to prevent overwhelming the network
122- await asyncio .sleep (0.5 )
123-
124- await self .send_response (message , msg_content )
124+ await self ._send_multiple_messages (message , messages )
125125
126126 return True
127127
@@ -134,32 +134,32 @@ def _load_channels_from_config(self, sub_command: str = None) -> dict:
134134 """Load channels from the Channels_List config section with optional sub-command filtering"""
135135 channels = {}
136136
137- if self .bot . config . has_section ( 'Channels_List' ):
138- for channel_name , description in self . bot . config . items ( 'Channels_List' ):
139- # Skip empty or commented lines
140- if channel_name . strip () and not channel_name . startswith ( '#' ):
141- # Strip quotes if present
142- if description . startswith ( '"' ) and description . endswith ( '"' ):
143- description = description [ 1 : - 1 ]
144-
145- # Handle sub-command filtering
146- if sub_command :
147- # Check if this channel belongs to the sub-command
148- if not channel_name .startswith (f'{ sub_command } .' ):
149- continue
150- # Remove the sub-command prefix for display
151- display_name = channel_name [len (sub_command ) + 1 :] # Remove 'subcommand.'
152- else :
153- # For general channels, only show channels that don't have sub-command prefixes
154- if '.' in channel_name :
155- continue
156- display_name = channel_name
157-
158- # Add # prefix if not already present
159- if not display_name .startswith ('#' ):
160- display_name = '#' + display_name
161-
162- channels [display_name ] = description
137+ for channel_name , description in self ._parse_config_channels ( ):
138+ # Handle sub-command filtering
139+ if sub_command :
140+ # Special case: "general" should show general channels (no category prefix)
141+ if sub_command == 'general' :
142+ # For general channels, only show channels that don't have sub-command prefixes
143+ if '.' in channel_name :
144+ continue
145+ display_name = channel_name
146+ else :
147+ # Check if this channel belongs to the sub-command
148+ if not channel_name .startswith (f'{ sub_command } .' ):
149+ continue
150+ # Remove the sub-command prefix for display
151+ display_name = channel_name [len (sub_command ) + 1 :] # Remove 'subcommand.'
152+ else :
153+ # For general channels, only show channels that don't have sub-command prefixes
154+ if '.' in channel_name :
155+ continue
156+ display_name = channel_name
157+
158+ # Add # prefix if not already present
159+ if not display_name .startswith ('#' ):
160+ display_name = '#' + display_name
161+
162+ channels [display_name ] = description
163163
164164 return channels
165165
@@ -181,10 +181,7 @@ async def _show_all_categories(self, message: MeshMessage):
181181 messages = self ._split_into_messages (category_list , "Available categories" )
182182
183183 # Send each message with a small delay between them
184- for i , msg_content in enumerate (messages ):
185- if i > 0 :
186- await asyncio .sleep (0.5 )
187- await self .send_response (message , msg_content )
184+ await self ._send_multiple_messages (message , messages )
188185
189186 except Exception as e :
190187 self .logger .error (f"Error showing categories: { e } " )
@@ -194,42 +191,36 @@ def _get_all_categories(self) -> dict:
194191 """Get all available channel categories and their channel counts"""
195192 categories = {}
196193
197- if self .bot .config .has_section ('Channels_List' ):
198- for channel_name , description in self .bot .config .items ('Channels_List' ):
199- # Skip empty or commented lines
200- if channel_name .strip () and not channel_name .startswith ('#' ):
201- # Check if this is a sub-command channel (has a dot)
202- if '.' in channel_name :
203- category = channel_name .split ('.' )[0 ]
204- if category not in categories :
205- categories [category ] = 0
206- categories [category ] += 1
207- else :
208- # General channels (no category)
209- if 'general' not in categories :
210- categories ['general' ] = 0
211- categories ['general' ] += 1
194+ for channel_name , description in self ._parse_config_channels ():
195+ # Check if this is a sub-command channel (has a dot)
196+ if '.' in channel_name :
197+ category = channel_name .split ('.' )[0 ]
198+ if category not in categories :
199+ categories [category ] = 0
200+ categories [category ] += 1
201+ else :
202+ # General channels (no category)
203+ if 'general' not in categories :
204+ categories ['general' ] = 0
205+ categories ['general' ] += 1
212206
213207 return categories
214208
215209 def _find_channel_by_name (self , search_name : str ) -> str :
216210 """Find a channel by partial name match across all categories"""
217211 search_name_lower = search_name .lower ()
218212
219- if self .bot .config .has_section ('Channels_List' ):
220- for config_name , description in self .bot .config .items ('Channels_List' ):
221- # Skip empty or commented lines
222- if config_name .strip () and not config_name .startswith ('#' ):
223- # Handle sub-command channels
224- if '.' in config_name :
225- category , name = config_name .split ('.' , 1 )
226- # Check if the channel name matches (case insensitive)
227- if name .lower () == search_name_lower :
228- return name
229- else :
230- # Check general channels
231- if config_name .lower () == search_name_lower :
232- return config_name
213+ for config_name , description in self ._parse_config_channels ():
214+ # Handle sub-command channels
215+ if '.' in config_name :
216+ category , name = config_name .split ('.' , 1 )
217+ # Check if the channel name matches (case insensitive)
218+ if name .lower () == search_name_lower :
219+ return name
220+ else :
221+ # Check general channels
222+ if config_name .lower () == search_name_lower :
223+ return config_name
233224
234225 return None
235226
@@ -240,22 +231,19 @@ async def _show_specific_channel(self, message: MeshMessage, channel_name: str):
240231 found_channel = None
241232 found_category = None
242233
243- if self .bot .config .has_section ('Channels_List' ):
244- for config_name , description in self .bot .config .items ('Channels_List' ):
245- # Skip empty or commented lines
246- if config_name .strip () and not config_name .startswith ('#' ):
247- # Handle sub-command channels
248- if '.' in config_name :
249- category , name = config_name .split ('.' , 1 )
250- display_name = '#' + name
251- else :
252- display_name = '#' + config_name
253-
254- # Check if this matches the requested channel
255- if display_name .lower () == channel_name .lower ():
256- found_channel = display_name
257- found_category = category if '.' in config_name else 'general'
258- break
234+ for config_name , description in self ._parse_config_channels ():
235+ # Handle sub-command channels
236+ if '.' in config_name :
237+ category , name = config_name .split ('.' , 1 )
238+ display_name = '#' + name
239+ else :
240+ display_name = '#' + config_name
241+
242+ # Check if this matches the requested channel
243+ if display_name .lower () == channel_name .lower ():
244+ found_channel = display_name
245+ found_category = category if '.' in config_name else 'general'
246+ break
259247
260248 if found_channel :
261249 # Get the description
@@ -284,23 +272,14 @@ def _split_into_messages(self, channel_list: list, sub_command: str = None) -> l
284272 messages = []
285273
286274 # Set appropriate header based on sub-command
287- if sub_command == "Available categories" :
288- current_message = "Available Categories: "
289- elif sub_command :
290- current_message = f"{ sub_command .title ()} : "
291- else :
292- current_message = "Common channels: "
293-
275+ current_message = self ._get_header_for_subcommand (sub_command )
294276 current_length = len (current_message )
295277
296278 for channel in channel_list :
297279 # Check if adding this channel would exceed the limit
298280 if current_length + len (channel ) + 2 > 130 : # +2 for ", " separator
299281 # Start a new message
300- if sub_command == "Available categories" :
301- expected_header = "Available Categories: "
302- else :
303- expected_header = f"{ sub_command .title ()} channels: " if sub_command else "Common channels: "
282+ expected_header = self ._get_continuation_header_for_subcommand (sub_command )
304283
305284 if current_message != expected_header :
306285 messages .append (current_message .rstrip (", " ))
@@ -314,21 +293,16 @@ def _split_into_messages(self, channel_list: list, sub_command: str = None) -> l
314293 continue
315294
316295 # Add channel to current message
317- if sub_command == "Available categories" :
318- header = "Available Categories: "
319- else :
320- header = f"{ sub_command .title ()} channels: " if sub_command else "Common channels: "
321- if current_message == header or current_message == "Channels (cont): " :
296+ initial_header = self ._get_header_for_subcommand (sub_command )
297+ continuation_header = self ._get_continuation_header_for_subcommand (sub_command )
298+ if current_message == initial_header or current_message == continuation_header or current_message == "Channels (cont): " :
322299 current_message += channel
323300 else :
324301 current_message += f", { channel } "
325302 current_length = len (current_message )
326303
327304 # Add the last message if it has content
328- if sub_command == "Available categories" :
329- header = "Available Categories: "
330- else :
331- header = f"{ sub_command .title ()} channels: " if sub_command else "Common channels: "
305+ header = self ._get_continuation_header_for_subcommand (sub_command )
332306 if current_message != header and current_message != "Channels (cont): " :
333307 messages .append (current_message )
334308
@@ -340,3 +314,56 @@ def _split_into_messages(self, channel_list: list, sub_command: str = None) -> l
340314 messages .append ("No channels configured" )
341315
342316 return messages
317+
318+ def _get_header_for_subcommand (self , sub_command : str = None ) -> str :
319+ """Get the appropriate header for a sub-command"""
320+ if sub_command == "Available categories" :
321+ return "Available Categories: "
322+ elif sub_command and sub_command != "general" :
323+ return f"{ sub_command .title ()} : "
324+ else :
325+ return "Common channels: "
326+
327+ def _get_continuation_header_for_subcommand (self , sub_command : str = None ) -> str :
328+ """Get the appropriate header for continuation messages"""
329+ if sub_command == "Available categories" :
330+ return "Available Categories: "
331+ elif sub_command and sub_command != "general" :
332+ return f"{ sub_command .title ()} channels: "
333+ else :
334+ return "Common channels: "
335+
336+ async def _send_multiple_messages (self , message : MeshMessage , messages : list ):
337+ """Send multiple messages with delays between them"""
338+ for i , msg_content in enumerate (messages ):
339+ if i > 0 :
340+ # Small delay between messages to prevent overwhelming the network
341+ await asyncio .sleep (0.5 )
342+ await self .send_response (message , msg_content )
343+
344+ def _parse_config_channels (self ):
345+ """Parse all channels from config, returning a generator of (name, description) tuples"""
346+ if not self .bot .config .has_section ('Channels_List' ):
347+ return
348+
349+ for channel_name , description in self .bot .config .items ('Channels_List' ):
350+ # Skip empty or commented lines
351+ if channel_name .strip () and not channel_name .startswith ('#' ):
352+ # Strip quotes if present
353+ if description .startswith ('"' ) and description .endswith ('"' ):
354+ description = description [1 :- 1 ]
355+ yield channel_name , description
356+
357+ def _is_valid_category (self , category_name : str ) -> bool :
358+ """Check if a category name is valid (has channels with that prefix)"""
359+ if not category_name :
360+ return False
361+
362+ # Check if there are any channels with this category prefix
363+ for channel_name , description in self ._parse_config_channels ():
364+ if '.' in channel_name :
365+ category = channel_name .split ('.' )[0 ]
366+ if category .lower () == category_name .lower ():
367+ return True
368+
369+ return False
0 commit comments