3838from .bot_config import BotConfig
3939from .commands import (
4040 BaseCommand ,
41- BaseCommandModel ,
41+ BotInfo ,
42+ Command ,
43+ CommandContext ,
4244 HelpCommand ,
4345 ScheduleCommand ,
4446 StatusCommand ,
47+ execute_command_func ,
48+ get_registered_commands ,
4549)
4650from .gateway import GatewayChannels , GatewayModule
4751from .structs import (
4852 Backend ,
4953 BotCommand ,
5054 BotMessage ,
55+ CommandVariant ,
5156)
5257
5358log = getLogger (__name__ )
@@ -70,18 +75,23 @@ class Bot(GatewayModule):
7075
7176 config : BotConfig
7277
73- _command_models : List [BaseCommandModel ] = PrivateAttr (default_factory = list )
74- _commands : Dict [str , BaseCommand ] = PrivateAttr (default_factory = dict )
78+ _command_models : List [Any ] = PrivateAttr (default_factory = list )
79+ _commands : Dict [str , Any ] = PrivateAttr (default_factory = dict )
7580 _configs : Dict [Backend , Any ] = PrivateAttr (default_factory = dict )
7681 _adapters : Dict [Backend , Any ] = PrivateAttr (default_factory = dict )
7782 _connected_backends : Dict [Backend , Tuple [Any , asyncio .AbstractEventLoop ]] = PrivateAttr (default_factory = dict )
7883 _scheduled : Dict [str , BotCommand ] = PrivateAttr (default_factory = dict )
7984 _authorized_users : Dict [Backend , Set [str ]] = PrivateAttr (default_factory = dict )
8085 _bot_user_ids : Dict [Backend , str ] = PrivateAttr (default_factory = dict )
8186 _bot_names : Dict [Backend , str ] = PrivateAttr (default_factory = dict )
87+ _deps : Any = PrivateAttr (default = None )
8288 _thread : Optional [threading .Thread ] = PrivateAttr (None )
8389 _lock : threading .Lock = PrivateAttr (default_factory = threading .Lock )
8490
91+ def set_deps (self , deps : Any ) -> None :
92+ """Set shared dependency object for new command framework contexts."""
93+ self ._deps = deps
94+
8595 def connect (self , channels : GatewayChannels ) -> None :
8696 """Connect to configured backends and set up message processing.
8797
@@ -318,8 +328,11 @@ async def _fetch() -> Optional[Channel]:
318328 log .exception (f"Error resolving channel: { channel_identifier } " )
319329 return None
320330
321- def load_commands (self , command_models : List [BaseCommandModel ]) -> None :
322- """Load command handlers from command models."""
331+ def load_commands (self , command_models : List [Any ]) -> None :
332+ """Load command handlers from command models and decorator registry.
333+
334+ Supports both legacy BaseCommandModel and the new CommandModel.
335+ """
323336 log .info (f"Loading { len (command_models )} commands..." )
324337 for model in command_models :
325338 try :
@@ -328,14 +341,59 @@ def load_commands(self, command_models: List[BaseCommandModel]) -> None:
328341 log .critical (f"Incomplete command type - implement all abstract methods: { model .command } " )
329342 raise e
330343
331- command_str = command .command ()
344+ if isinstance (command , BaseCommand ):
345+ command_str = command .command ()
346+ runner : Any = command
347+ elif isinstance (command , Command ):
348+ command_str = command .name
349+ runner = command
350+ else :
351+ raise TypeError (f"Unsupported command type from model { type (model ).__name__ } : { type (command ).__name__ } " )
352+
332353 log .info (f"Registered command: /{ command_str } " )
333354 if command_str in self ._commands :
334355 raise Exception (f"Command already registered: { command_str } \n \t { command } \n \t { self ._commands [command_str ]} " )
335356
336- self ._commands [command_str ] = command
357+ self ._commands [command_str ] = runner
337358 self ._command_models .append (model )
338359
360+ # Decorator-registered commands are available globally and can be mixed
361+ # with model-based commands. Explicit model definitions win on conflicts.
362+ for command_name , entry in get_registered_commands ().items ():
363+ if command_name in self ._commands :
364+ continue
365+ log .info (f"Registered decorated command: /{ command_name } " )
366+ self ._commands [command_name ] = entry
367+
368+ def _command_backends (self , command_runner : Any ) -> List [str ]:
369+ """Return supported backends for either legacy or new command types."""
370+ if isinstance (command_runner , BaseCommand ):
371+ return command_runner .backends ()
372+ if isinstance (command_runner , Command ):
373+ return command_runner .backends
374+ return list (getattr (command_runner , "backends" , []) or [])
375+
376+ def _build_command_context (self , cmd : BotCommand ) -> CommandContext :
377+ """Build CommandContext from legacy BotCommand for new framework execution."""
378+ bot_info = BotInfo (
379+ id = self ._get_bot_id (cmd .backend ) or "" ,
380+ name = self ._get_bot_name (cmd .backend ) or "" ,
381+ version = "" ,
382+ )
383+ channel = Channel (id = cmd .channel_id , name = cmd .channel_name )
384+ return CommandContext (
385+ command_name = cmd .command ,
386+ source = cmd .source ,
387+ targets = list (cmd .targets ),
388+ channel = channel ,
389+ message = cmd .message ,
390+ args = list (cmd .args ),
391+ args_text = " " .join (cmd .args ),
392+ backend = cmd .backend ,
393+ bot = bot_info ,
394+ deps = self ._deps ,
395+ )
396+
339397 # =========================================================================
340398 # Message Processing Nodes
341399 # =========================================================================
@@ -733,7 +791,8 @@ def _extract_commands(
733791 command_runner = self ._commands [command_name ]
734792
735793 # Check backend support
736- if command_runner .backends () and backend not in command_runner .backends ():
794+ command_backends = self ._command_backends (command_runner )
795+ if command_backends and backend not in command_backends :
737796 log .warning (f"Command { command_name } not supported on { backend } " )
738797 return None
739798
@@ -764,7 +823,7 @@ def _extract_commands(
764823 channel_id = target_channel ,
765824 channel_name = channel_name ,
766825 backend = backend ,
767- variant = command_runner .kind (),
826+ variant = command_runner .kind () if isinstance ( command_runner , BaseCommand ) else CommandVariant . REPLY ,
768827 message = msg ,
769828 delay = None ,
770829 schedule = "" ,
@@ -874,7 +933,7 @@ def _create_help_command(self, msg: Message, backend: str, channel_id: str) -> B
874933 channel_id = channel_id ,
875934 channel_name = channel_name ,
876935 backend = backend ,
877- variant = command_runner .kind (),
936+ variant = command_runner .kind () if isinstance ( command_runner , BaseCommand ) else CommandVariant . REPLY ,
878937 message = msg ,
879938 delay = None ,
880939 schedule = "" ,
@@ -888,12 +947,22 @@ def _execute_command(self, cmd: BotCommand) -> Optional[Union[Message, List[Mess
888947 return None
889948
890949 try :
891- if isinstance (command_runner , HelpCommand ):
892- responses = command_runner .execute (cmd , MappingProxyType (self ._commands ))
893- elif isinstance (command_runner , ScheduleCommand ):
894- responses = command_runner .execute (cmd , self ._scheduled )
950+ if isinstance (command_runner , BaseCommand ):
951+ if isinstance (command_runner , HelpCommand ):
952+ responses = command_runner .execute (cmd , MappingProxyType (self ._commands ))
953+ elif isinstance (command_runner , ScheduleCommand ):
954+ responses = command_runner .execute (cmd , self ._scheduled )
955+ else :
956+ responses = command_runner .execute (cmd )
957+ elif isinstance (command_runner , Command ):
958+ ctx = self ._build_command_context (cmd )
959+ responses = [r for r in execute_command_func (command_runner .execute , ctx ) if r is not None ]
960+ elif hasattr (command_runner , "handler" ):
961+ ctx = self ._build_command_context (cmd )
962+ responses = [r for r in execute_command_func (command_runner .handler , ctx ) if r is not None ]
895963 else :
896- responses = command_runner .execute (cmd )
964+ log .error (f"Unsupported command runner type for { cmd .command } : { type (command_runner ).__name__ } " )
965+ return None
897966 except Exception :
898967 log .exception (f"Error executing command: { cmd .command } " )
899968 return None
0 commit comments