1010from ably .transport .websockettransport import ProtocolMessageAction
1111from ably .types .channelstate import ChannelState , ChannelStateChange
1212from ably .types .flags import Flag , has_flag
13- from ably .types .message import Message
13+ from ably .types .message import Message , MessageAction , MessageVersion
1414from ably .types .mixins import DecodingContext
15+ from ably .types .operations import MessageOperation , PublishResult , UpdateDeleteResult
1516from ably .types .presence import PresenceMessage
1617from ably .util .eventemitter import EventEmitter
1718from ably .util .exceptions import AblyException , IncompatibleClientIdException
@@ -390,7 +391,7 @@ def unsubscribe(self, *args) -> None:
390391 self .__message_emitter .off (listener )
391392
392393 # RTL6
393- async def publish (self , * args , ** kwargs ) -> None :
394+ async def publish (self , * args , ** kwargs ) -> PublishResult :
394395 """Publish a message or messages on this channel
395396
396397 Publishes a single message or an array of messages to the channel.
@@ -490,7 +491,7 @@ async def publish(self, *args, **kwargs) -> None:
490491 }
491492
492493 # RTL6b: Await acknowledgment from server
493- await self .__realtime .connection .connection_manager .send_protocol_message (protocol_message )
494+ return await self .__realtime .connection .connection_manager .send_protocol_message (protocol_message )
494495
495496 def _throw_if_unpublishable_state (self ) -> None :
496497 """Check if the channel and connection are in a state that allows publishing
@@ -522,6 +523,200 @@ def _throw_if_unpublishable_state(self) -> None:
522523 90001 ,
523524 )
524525
526+ async def _send_update (self , message : Message , action : MessageAction ,
527+ operation : MessageOperation = None ) -> UpdateDeleteResult :
528+ """Internal method to send update/delete/append operations via websocket.
529+
530+ Parameters
531+ ----------
532+ message : Message
533+ Message object with serial field required
534+ action : MessageAction
535+ The action type (MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_APPEND)
536+ operation : MessageOperation, optional
537+ Operation metadata (description, metadata)
538+
539+ Returns
540+ -------
541+ UpdateDeleteResult
542+ Result containing version serial of the operation
543+
544+ Raises
545+ ------
546+ AblyException
547+ If message serial is missing or connection/channel state prevents operation
548+ """
549+ # Check message has serial
550+ if not message .serial :
551+ raise AblyException (
552+ "Message serial is required for update/delete/append operations" ,
553+ 400 ,
554+ 40000
555+ )
556+
557+ # Check connection and channel state
558+ self ._throw_if_unpublishable_state ()
559+
560+ # Create version from operation if provided
561+ if not operation :
562+ version = None
563+ else :
564+ version = MessageVersion (
565+ client_id = operation .client_id ,
566+ description = operation .description ,
567+ metadata = operation .metadata
568+ )
569+
570+ # Create a new message with the operation fields
571+ update_message = Message (
572+ name = message .name ,
573+ data = message .data ,
574+ client_id = message .client_id ,
575+ serial = message .serial ,
576+ action = action ,
577+ version = version ,
578+ )
579+
580+ # Encrypt if needed
581+ if self .cipher :
582+ update_message .encrypt (self .cipher )
583+
584+ # Convert to dict representation
585+ msg_dict = update_message .as_dict (binary = self .ably .options .use_binary_protocol )
586+
587+ log .info (
588+ f'RealtimeChannel._send_update(): sending { action .name } message; '
589+ f'channel = { self .name } , state = { self .state } , serial = { message .serial } '
590+ )
591+
592+ # Send protocol message
593+ protocol_message = {
594+ "action" : ProtocolMessageAction .MESSAGE ,
595+ "channel" : self .name ,
596+ "messages" : [msg_dict ],
597+ }
598+
599+ # Send and await acknowledgment
600+ result = await self .__realtime .connection .connection_manager .send_protocol_message (protocol_message )
601+
602+ # Return UpdateDeleteResult - we don't have version_serial from the result yet
603+ # The server will send ACK with the result
604+ if result and hasattr (result , 'serials' ) and result .serials :
605+ return UpdateDeleteResult (version_serial = result .serials [0 ])
606+ return UpdateDeleteResult ()
607+
608+ async def update_message (self , message : Message , operation : MessageOperation = None ) -> UpdateDeleteResult :
609+ """Updates an existing message on this channel.
610+
611+ Parameters
612+ ----------
613+ message : Message
614+ Message object to update. Must have a serial field.
615+ operation : MessageOperation, optional
616+ Optional MessageOperation containing description and metadata for the update.
617+
618+ Returns
619+ -------
620+ UpdateDeleteResult
621+ Result containing the version serial of the updated message.
622+
623+ Raises
624+ ------
625+ AblyException
626+ If message serial is missing or connection/channel state prevents the update
627+ """
628+ return await self ._send_update (message , MessageAction .MESSAGE_UPDATE , operation )
629+
630+ async def delete_message (self , message : Message , operation : MessageOperation = None ) -> UpdateDeleteResult :
631+ """Deletes a message on this channel.
632+
633+ Parameters
634+ ----------
635+ message : Message
636+ Message object to delete. Must have a serial field.
637+ operation : MessageOperation, optional
638+ Optional MessageOperation containing description and metadata for the delete.
639+
640+ Returns
641+ -------
642+ UpdateDeleteResult
643+ Result containing the version serial of the deleted message.
644+
645+ Raises
646+ ------
647+ AblyException
648+ If message serial is missing or connection/channel state prevents the delete
649+ """
650+ return await self ._send_update (message , MessageAction .MESSAGE_DELETE , operation )
651+
652+ async def append_message (self , message : Message , operation : MessageOperation = None ) -> UpdateDeleteResult :
653+ """Appends data to an existing message on this channel.
654+
655+ Parameters
656+ ----------
657+ message : Message
658+ Message object with data to append. Must have a serial field.
659+ operation : MessageOperation, optional
660+ Optional MessageOperation containing description and metadata for the append.
661+
662+ Returns
663+ -------
664+ UpdateDeleteResult
665+ Result containing the version serial of the appended message.
666+
667+ Raises
668+ ------
669+ AblyException
670+ If message serial is missing or connection/channel state prevents the append
671+ """
672+ return await self ._send_update (message , MessageAction .MESSAGE_APPEND , operation )
673+
674+ async def get_message (self , serial_or_message , timeout = None ):
675+ """Retrieves a single message by its serial using the REST API.
676+
677+ Parameters
678+ ----------
679+ serial_or_message : str or Message
680+ Either a string serial or a Message object with a serial field.
681+ timeout : float, optional
682+ Timeout for the request.
683+
684+ Returns
685+ -------
686+ Message
687+ Message object for the requested serial.
688+
689+ Raises
690+ ------
691+ AblyException
692+ If the serial is missing or the message cannot be retrieved.
693+ """
694+ # Delegate to parent Channel (REST) implementation
695+ return await Channel .get_message (self , serial_or_message , timeout = timeout )
696+
697+ async def get_message_versions (self , serial_or_message , params = None ):
698+ """Retrieves version history for a message using the REST API.
699+
700+ Parameters
701+ ----------
702+ serial_or_message : str or Message
703+ Either a string serial or a Message object with a serial field.
704+ params : dict, optional
705+ Optional dict of query parameters for pagination.
706+
707+ Returns
708+ -------
709+ PaginatedResult
710+ PaginatedResult containing Message objects representing each version.
711+
712+ Raises
713+ ------
714+ AblyException
715+ If the serial is missing or versions cannot be retrieved.
716+ """
717+ # Delegate to parent Channel (REST) implementation
718+ return await Channel .get_message_versions (self , serial_or_message , params = params )
719+
525720 def _on_message (self , proto_msg : dict ) -> None :
526721 action = proto_msg .get ('action' )
527722 # RTL4c1
@@ -766,7 +961,7 @@ class Channels(RestChannels):
766961 """
767962
768963 # RTS3
769- def get (self , name : str , options : ChannelOptions | None = None ) -> RealtimeChannel :
964+ def get (self , name : str , options : ChannelOptions | None = None , ** kwargs ) -> RealtimeChannel :
770965 """Creates a new RealtimeChannel object, or returns the existing channel object.
771966
772967 Parameters
@@ -776,7 +971,15 @@ def get(self, name: str, options: ChannelOptions | None = None) -> RealtimeChann
776971 Channel name
777972 options: ChannelOptions or dict, optional
778973 Channel options for the channel
974+ **kwargs:
975+ Additional keyword arguments to create ChannelOptions (e.g., cipher, params)
779976 """
977+ # Convert kwargs to ChannelOptions if provided
978+ if kwargs and not options :
979+ options = ChannelOptions (** kwargs )
980+ elif options and isinstance (options , dict ):
981+ options = ChannelOptions .from_dict (options )
982+
780983 if name not in self .__all :
781984 channel = self .__all [name ] = RealtimeChannel (self .__ably , name , options )
782985 else :
0 commit comments