comparison libervia/backend/core/xmpp.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 2417ad1d0f23
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
89 return partial(getattr(self.plugin, attr), self.client) 89 return partial(getattr(self.plugin, attr), self.client)
90 90
91 91
92 class SatXMPPEntity(core_types.SatXMPPEntity): 92 class SatXMPPEntity(core_types.SatXMPPEntity):
93 """Common code for Client and Component""" 93 """Common code for Client and Component"""
94
94 # profile is added there when start_connection begins and removed when it is finished 95 # profile is added there when start_connection begins and removed when it is finished
95 profiles_connecting = set() 96 profiles_connecting = set()
96 97
97 def __init__(self, host_app, profile, max_retries): 98 def __init__(self, host_app, profile, max_retries):
98 factory = self.factory 99 factory = self.factory
100 # we monkey patch clientConnectionLost to handle network_enabled/network_disabled 101 # we monkey patch clientConnectionLost to handle network_enabled/network_disabled
101 # and to allow plugins to tune reconnection mechanism 102 # and to allow plugins to tune reconnection mechanism
102 clientConnectionFailed_ori = factory.clientConnectionFailed 103 clientConnectionFailed_ori = factory.clientConnectionFailed
103 clientConnectionLost_ori = factory.clientConnectionLost 104 clientConnectionLost_ori = factory.clientConnectionLost
104 factory.clientConnectionFailed = partial( 105 factory.clientConnectionFailed = partial(
105 self.connection_terminated, term_type="failed", cb=clientConnectionFailed_ori) 106 self.connection_terminated, term_type="failed", cb=clientConnectionFailed_ori
107 )
106 factory.clientConnectionLost = partial( 108 factory.clientConnectionLost = partial(
107 self.connection_terminated, term_type="lost", cb=clientConnectionLost_ori) 109 self.connection_terminated, term_type="lost", cb=clientConnectionLost_ori
110 )
108 111
109 factory.maxRetries = max_retries 112 factory.maxRetries = max_retries
110 factory.maxDelay = 30 113 factory.maxDelay = 30
111 # when self._connected_d is None, we are not connected 114 # when self._connected_d is None, we are not connected
112 # else, it's a deferred which fire on disconnection 115 # else, it's a deferred which fire on disconnection
113 self._connected_d = None 116 self._connected_d = None
114 self.profile = profile 117 self.profile = profile
115 self.host_app = host_app 118 self.host_app = host_app
116 self.cache = cache.Cache(host_app, profile) 119 self.cache = cache.Cache(host_app, profile)
117 self.mess_id2uid = {} # map from message id to uid used in history. 120 self.mess_id2uid = {} # map from message id to uid used in history.
118 # Key: (full_jid, message_id) Value: uid 121 # Key: (full_jid, message_id) Value: uid
119 # this Deferred fire when entity is connected 122 # this Deferred fire when entity is connected
120 self.conn_deferred = defer.Deferred() 123 self.conn_deferred = defer.Deferred()
121 self._progress_cb = {} # callback called when a progress is requested 124 self._progress_cb = {} # callback called when a progress is requested
122 # (key = progress id) 125 # (key = progress id)
123 self.actions = {} # used to keep track of actions for retrieval (key = action_id) 126 self.actions = {} # used to keep track of actions for retrieval (key = action_id)
124 self.encryption = encryption.EncryptionHandler(self) 127 self.encryption = encryption.EncryptionHandler(self)
125 128
126 def __str__(self): 129 def __str__(self):
127 return f"Client for profile {self.profile}" 130 return f"Client for profile {self.profile}"
146 if plugin.is_handler: 149 if plugin.is_handler:
147 plugin.get_handler(self).setHandlerParent(self) 150 plugin.get_handler(self).setHandlerParent(self)
148 151
149 # profile_connecting/profile_connected methods handling 152 # profile_connecting/profile_connected methods handling
150 153
151 timer = connection_timer[plugin] = { 154 timer = connection_timer[plugin] = {"total": 0}
152 "total": 0
153 }
154 # profile connecting is called right now (before actually starting client) 155 # profile connecting is called right now (before actually starting client)
155 connecting_cb = getattr(plugin, "profile_connecting", None) 156 connecting_cb = getattr(plugin, "profile_connecting", None)
156 if connecting_cb is not None: 157 if connecting_cb is not None:
157 connecting_start = time.time() 158 connecting_start = time.time()
158 await utils.as_deferred(connecting_cb, self) 159 await utils.as_deferred(connecting_cb, self)
185 """ 186 """
186 return 187 return
187 188
188 @staticmethod 189 @staticmethod
189 async def _run_profile_connected( 190 async def _run_profile_connected(
190 callback: Callable, 191 callback: Callable, entity: "SatXMPPEntity", timer: Dict[str, float]
191 entity: "SatXMPPEntity",
192 timer: Dict[str, float]
193 ) -> None: 192 ) -> None:
194 connected_start = time.time() 193 connected_start = time.time()
195 await utils.as_deferred(callback, entity) 194 await utils.as_deferred(callback, entity)
196 timer["connected"] = time.time() - connected_start 195 timer["connected"] = time.time() - connected_start
197 timer["total"] += timer["connected"] 196 timer["total"] += timer["connected"]
215 C.FORCE_PORT_PARAM, "Connection", profile_key=profile 214 C.FORCE_PORT_PARAM, "Connection", profile_key=profile
216 ) 215 )
217 ) 216 )
218 except ValueError: 217 except ValueError:
219 log.debug(_("Can't parse port value, using default value")) 218 log.debug(_("Can't parse port value, using default value"))
220 port = ( 219 port = None # will use default value 5222 or be retrieved from a DNS SRV record
221 None
222 ) # will use default value 5222 or be retrieved from a DNS SRV record
223 220
224 password = await host.memory.param_get_a_async( 221 password = await host.memory.param_get_a_async(
225 "Password", "Connection", profile_key=profile 222 "Password", "Connection", profile_key=profile
226 ) 223 )
227 224
228 entity_jid_s = await host.memory.param_get_a_async( 225 entity_jid_s = await host.memory.param_get_a_async(
229 "JabberID", "Connection", profile_key=profile) 226 "JabberID", "Connection", profile_key=profile
227 )
230 entity_jid = jid.JID(entity_jid_s) 228 entity_jid = jid.JID(entity_jid_s)
231 229
232 if not entity_jid.resource and not cls.is_component and entity_jid.user: 230 if not entity_jid.resource and not cls.is_component and entity_jid.user:
233 # if no resource is specified, we create our own instead of using 231 # if no resource is specified, we create our own instead of using
234 # server returned one, as it will then stay stable in case of 232 # server returned one, as it will then stay stable in case of
235 # reconnection. we only do that for client and if there is a user part, to 233 # reconnection. we only do that for client and if there is a user part, to
236 # let server decide for anonymous login 234 # let server decide for anonymous login
237 resource_dict = await host.memory.storage.get_privates( 235 resource_dict = await host.memory.storage.get_privates(
238 "core:xmpp", ["resource"] , profile=profile) 236 "core:xmpp", ["resource"], profile=profile
237 )
239 try: 238 try:
240 resource = resource_dict["resource"] 239 resource = resource_dict["resource"]
241 except KeyError: 240 except KeyError:
242 resource = f"{C.APP_NAME_FILE}.{shortuuid.uuid()}" 241 resource = f"{C.APP_NAME_FILE}.{shortuuid.uuid()}"
243 await host.memory.storage.set_private_value( 242 await host.memory.storage.set_private_value(
244 "core:xmpp", "resource", resource, profile=profile) 243 "core:xmpp", "resource", resource, profile=profile
245 244 )
246 log.info(_("We'll use the stable resource {resource}").format( 245
247 resource=resource)) 246 log.info(
247 _("We'll use the stable resource {resource}").format(
248 resource=resource
249 )
250 )
248 entity_jid.resource = resource 251 entity_jid.resource = resource
249 252
250 if profile in host.profiles: 253 if profile in host.profiles:
251 if host.profiles[profile].is_connected(): 254 if host.profiles[profile].is_connected():
252 raise exceptions.InternalError( 255 raise exceptions.InternalError(
253 f"There is already a connected profile of name {profile!r} in " 256 f"There is already a connected profile of name {profile!r} in "
254 f"host") 257 f"host"
255 log.debug( 258 )
256 "removing unconnected profile {profile!r}") 259 log.debug("removing unconnected profile {profile!r}")
257 del host.profiles[profile] 260 del host.profiles[profile]
258 entity = host.profiles[profile] = cls( 261 entity = host.profiles[profile] = cls(
259 host, profile, entity_jid, password, 262 host,
260 host.memory.param_get_a(C.FORCE_SERVER_PARAM, "Connection", 263 profile,
261 profile_key=profile) or None, 264 entity_jid,
262 port, max_retries, 265 password,
263 ) 266 host.memory.param_get_a(
267 C.FORCE_SERVER_PARAM, "Connection", profile_key=profile
268 )
269 or None,
270 port,
271 max_retries,
272 )
264 273
265 await entity.encryption.load_sessions() 274 await entity.encryption.load_sessions()
266 275
267 entity._create_sub_protocols() 276 entity._create_sub_protocols()
268 277
313 322
314 log.debug(f"Plugin loading time for {profile!r} (longer to shorter):\n") 323 log.debug(f"Plugin loading time for {profile!r} (longer to shorter):\n")
315 plugins_by_timer = sorted( 324 plugins_by_timer = sorted(
316 connection_timer, 325 connection_timer,
317 key=lambda p: connection_timer[p]["total"], 326 key=lambda p: connection_timer[p]["total"],
318 reverse=True 327 reverse=True,
319 ) 328 )
320 # total is the addition of all connecting and connected, doesn't really 329 # total is the addition of all connecting and connected, doesn't really
321 # reflect the real loading time as connected are launched in a 330 # reflect the real loading time as connected are launched in a
322 # DeferredList 331 # DeferredList
323 total_plugins = 0 332 total_plugins = 0
439 anymore 448 anymore
440 """ 449 """
441 # we save connector because it may be deleted when connection will be dropped 450 # we save connector because it may be deleted when connection will be dropped
442 # if reconnection is disabled 451 # if reconnection is disabled
443 self._saved_connector = connector 452 self._saved_connector = connector
444 if reason is not None and not isinstance(reason.value, 453 if reason is not None and not isinstance(
445 internet_error.ConnectionDone): 454 reason.value, internet_error.ConnectionDone
455 ):
446 try: 456 try:
447 reason_str = str(reason.value) 457 reason_str = str(reason.value)
448 except Exception: 458 except Exception:
449 # FIXME: workaround for Android were p4a strips docstrings 459 # FIXME: workaround for Android were p4a strips docstrings
450 # while Twisted use docstring in __str__ 460 # while Twisted use docstring in __str__
494 connector.connect() 504 connector.connect()
495 505
496 def _connected(self, xs): 506 def _connected(self, xs):
497 send_hooks = [] 507 send_hooks = []
498 receive_hooks = [] 508 receive_hooks = []
499 self.host_app.trigger.point( 509 self.host_app.trigger.point("stream_hooks", self, receive_hooks, send_hooks)
500 "stream_hooks", self, receive_hooks, send_hooks)
501 for hook in receive_hooks: 510 for hook in receive_hooks:
502 xs.add_hook(C.STREAM_HOOK_RECEIVE, hook) 511 xs.add_hook(C.STREAM_HOOK_RECEIVE, hook)
503 for hook in send_hooks: 512 for hook in send_hooks:
504 xs.add_hook(C.STREAM_HOOK_SEND, hook) 513 xs.add_hook(C.STREAM_HOOK_SEND, hook)
505 super(SatXMPPEntity, self)._connected(xs) 514 super(SatXMPPEntity, self)._connected(xs)
527 else: 536 else:
528 err = reason 537 err = reason
529 try: 538 try:
530 if err.value.args[0][0][2] == "certificate verify failed": 539 if err.value.args[0][0][2] == "certificate verify failed":
531 err = exceptions.InvalidCertificate( 540 err = exceptions.InvalidCertificate(
532 _("Your server certificate is not valid " 541 _(
533 "(its identity can't be checked).\n\n" 542 "Your server certificate is not valid "
534 "This should never happen and may indicate that " 543 "(its identity can't be checked).\n\n"
535 "somebody is trying to spy on you.\n" 544 "This should never happen and may indicate that "
536 "Please contact your server administrator.")) 545 "somebody is trying to spy on you.\n"
546 "Please contact your server administrator."
547 )
548 )
537 self.factory.stopTrying() 549 self.factory.stopTrying()
538 try: 550 try:
539 # with invalid certificate, we should not retry to connect 551 # with invalid certificate, we should not retry to connect
540 # so we delete saved connector to avoid reconnection if 552 # so we delete saved connector to avoid reconnection if
541 # network_enabled is called. 553 # network_enabled is called.
613 self.xmlstream.send(iq_error_elt) 625 self.xmlstream.send(iq_error_elt)
614 626
615 def generate_message_xml( 627 def generate_message_xml(
616 self, 628 self,
617 data: core_types.MessageData, 629 data: core_types.MessageData,
618 post_xml_treatments: Optional[defer.Deferred] = None 630 post_xml_treatments: Optional[defer.Deferred] = None,
619 ) -> core_types.MessageData: 631 ) -> core_types.MessageData:
620 """Generate <message/> stanza from message data 632 """Generate <message/> stanza from message data
621 633
622 @param data: message data 634 @param data: message data
623 domish element will be put in data['xml'] 635 domish element will be put in data['xml']
708 # XXX: This is the last trigger before u"send" (last but one globally) 720 # XXX: This is the last trigger before u"send" (last but one globally)
709 # for sending message. 721 # for sending message.
710 # This is intented for e2e encryption which doesn't do full stanza 722 # This is intented for e2e encryption which doesn't do full stanza
711 # encryption (e.g. OTR) 723 # encryption (e.g. OTR)
712 # This trigger point can't cancel the method 724 # This trigger point can't cancel the method
713 await self.host_app.trigger.async_point("send_message_data", self, mess_data, 725 await self.host_app.trigger.async_point(
714 triggers_no_cancel=True) 726 "send_message_data", self, mess_data, triggers_no_cancel=True
727 )
715 await self.a_send(mess_data["xml"]) 728 await self.a_send(mess_data["xml"])
716 return mess_data 729 return mess_data
717 730
718 def sendMessage( 731 def sendMessage(
719 self, to_jid, message, subject=None, mess_type="auto", extra=None, uid=None, 732 self,
720 no_trigger=False): 733 to_jid,
734 message,
735 subject=None,
736 mess_type="auto",
737 extra=None,
738 uid=None,
739 no_trigger=False,
740 ):
721 r"""Send a message to an entity 741 r"""Send a message to an entity
722 742
723 @param to_jid(jid.JID): destinee of the message 743 @param to_jid(jid.JID): destinee of the message
724 @param message(dict): message body, key is the language (use '' when unknown) 744 @param message(dict): message body, key is the language (use '' when unknown)
725 @param subject(dict): message subject, key is the language (use '' when unknown) 745 @param subject(dict): message subject, key is the language (use '' when unknown)
795 pre_xml_treatments, 815 pre_xml_treatments,
796 post_xml_treatments, 816 post_xml_treatments,
797 ): 817 ):
798 return defer.succeed(None) 818 return defer.succeed(None)
799 819
800 log.debug(_("Sending message (type {type}, to {to})") 820 log.debug(
801 .format(type=data["type"], to=to_jid.full())) 821 _("Sending message (type {type}, to {to})").format(
802 822 type=data["type"], to=to_jid.full()
803 pre_xml_treatments.addCallback(lambda __: self.generate_message_xml(data, post_xml_treatments)) 823 )
824 )
825
826 pre_xml_treatments.addCallback(
827 lambda __: self.generate_message_xml(data, post_xml_treatments)
828 )
804 pre_xml_treatments.addCallback(lambda __: post_xml_treatments) 829 pre_xml_treatments.addCallback(lambda __: post_xml_treatments)
805 pre_xml_treatments.addErrback(self._cancel_error_trap) 830 pre_xml_treatments.addErrback(self._cancel_error_trap)
806 post_xml_treatments.addCallback( 831 post_xml_treatments.addCallback(
807 lambda __: defer.ensureDeferred(self.send_message_data(data)) 832 lambda __: defer.ensureDeferred(self.send_message_data(data))
808 ) 833 )
809 if send_only: 834 if send_only:
810 log.debug(_("Triggers, storage and echo have been inhibited by the " 835 log.debug(
811 "'send_only' parameter")) 836 _(
837 "Triggers, storage and echo have been inhibited by the "
838 "'send_only' parameter"
839 )
840 )
812 else: 841 else:
813 self.add_post_xml_callbacks(post_xml_treatments) 842 self.add_post_xml_callbacks(post_xml_treatments)
814 post_xml_treatments.addErrback(self._cancel_error_trap) 843 post_xml_treatments.addErrback(self._cancel_error_trap)
815 post_xml_treatments.addErrback(self.host_app.log_errback) 844 post_xml_treatments.addErrback(self.host_app.log_errback)
816 pre_xml_treatments.callback(data) 845 pre_xml_treatments.callback(data)
821 failure.trap(exceptions.CancelError) 850 failure.trap(exceptions.CancelError)
822 851
823 def is_message_printable(self, mess_data): 852 def is_message_printable(self, mess_data):
824 """Return True if a message contain payload to show in frontends""" 853 """Return True if a message contain payload to show in frontends"""
825 return ( 854 return (
826 mess_data["message"] or mess_data["subject"] 855 mess_data["message"]
856 or mess_data["subject"]
827 or mess_data["extra"].get(C.KEY_ATTACHMENTS) 857 or mess_data["extra"].get(C.KEY_ATTACHMENTS)
828 or mess_data["type"] == C.MESS_TYPE_INFO 858 or mess_data["type"] == C.MESS_TYPE_INFO
829 ) 859 )
830 860
831 async def message_add_to_history(self, data): 861 async def message_add_to_history(self, data):
847 ) # empty body should be managed by plugins before this point 877 ) # empty body should be managed by plugins before this point
848 return data 878 return data
849 879
850 def message_get_bridge_args(self, data): 880 def message_get_bridge_args(self, data):
851 """Generate args to use with bridge from data dict""" 881 """Generate args to use with bridge from data dict"""
852 return (data["uid"], data["timestamp"], data["from"].full(), 882 return (
853 data["to"].full(), data["message"], data["subject"], 883 data["uid"],
854 data["type"], data_format.serialise(data["extra"])) 884 data["timestamp"],
855 885 data["from"].full(),
886 data["to"].full(),
887 data["message"],
888 data["subject"],
889 data["type"],
890 data_format.serialise(data["extra"]),
891 )
856 892
857 def message_send_to_bridge(self, data): 893 def message_send_to_bridge(self, data):
858 """Send message to bridge, so frontends can display it 894 """Send message to bridge, so frontends can display it
859 895
860 @param data: message data dictionnary 896 @param data: message data dictionnary
867 # we need a message to send something 903 # we need a message to send something
868 if self.is_message_printable(data): 904 if self.is_message_printable(data):
869 905
870 # We send back the message, so all frontends are aware of it 906 # We send back the message, so all frontends are aware of it
871 self.host_app.bridge.message_new( 907 self.host_app.bridge.message_new(
872 *self.message_get_bridge_args(data), 908 *self.message_get_bridge_args(data), profile=self.profile
873 profile=self.profile
874 ) 909 )
875 else: 910 else:
876 log.warning(_("No message found")) 911 log.warning(_("No message found"))
877 return data 912 return data
878 913
898 @implementer(iwokkel.IDisco) 933 @implementer(iwokkel.IDisco)
899 class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient): 934 class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
900 trigger_suffix = "" 935 trigger_suffix = ""
901 is_component = False 936 is_component = False
902 937
903 def __init__(self, host_app, profile, user_jid, password, host=None, 938 def __init__(
904 port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES): 939 self,
940 host_app,
941 profile,
942 user_jid,
943 password,
944 host=None,
945 port=C.XMPP_C2S_PORT,
946 max_retries=C.XMPP_MAX_RETRIES,
947 ):
905 # XXX: DNS SRV records are checked when the host is not specified. 948 # XXX: DNS SRV records are checked when the host is not specified.
906 # If no SRV record is found, the host is directly extracted from the JID. 949 # If no SRV record is found, the host is directly extracted from the JID.
907 self.started = time.time() 950 self.started = time.time()
908 951
909 # Currently, we use "client/pc/Salut à Toi", but as 952 # Currently, we use "client/pc/Salut à Toi", but as
931 _("invalid data used for host: {data}").format(data=host_data) 974 _("invalid data used for host: {data}").format(data=host_data)
932 ) 975 )
933 host_data = None 976 host_data = None
934 if host_data is not None: 977 if host_data is not None:
935 log.info( 978 log.info(
936 "using {host}:{port} for host {host_ori} as requested in config" 979 "using {host}:{port} for host {host_ori} as requested in config".format(
937 .format(host_ori=user_jid.host, host=host, port=port) 980 host_ori=user_jid.host, host=host, port=port
981 )
938 ) 982 )
939 983
940 self.check_certificate = host_app.memory.param_get_a( 984 self.check_certificate = host_app.memory.param_get_a(
941 "check_certificate", "Connection", profile_key=profile) 985 "check_certificate", "Connection", profile_key=profile
986 )
942 987
943 if self.check_certificate: 988 if self.check_certificate:
944 tls_required, configurationForTLS = True, None 989 tls_required, configurationForTLS = True, None
945 else: 990 else:
946 tls_required = False 991 tls_required = False
947 configurationForTLS = ssl.CertificateOptions(trustRoot=None) 992 configurationForTLS = ssl.CertificateOptions(trustRoot=None)
948 993
949 wokkel_client.XMPPClient.__init__( 994 wokkel_client.XMPPClient.__init__(
950 self, user_jid, password, host or None, port or C.XMPP_C2S_PORT, 995 self,
951 tls_required=tls_required, configurationForTLS=configurationForTLS 996 user_jid,
997 password,
998 host or None,
999 port or C.XMPP_C2S_PORT,
1000 tls_required=tls_required,
1001 configurationForTLS=configurationForTLS,
952 ) 1002 )
953 SatXMPPEntity.__init__(self, host_app, profile, max_retries) 1003 SatXMPPEntity.__init__(self, host_app, profile, max_retries)
954 1004
955 if not self.check_certificate: 1005 if not self.check_certificate:
956 msg = (_("Certificate validation is deactivated, this is unsecure and " 1006 msg = _(
1007 "Certificate validation is deactivated, this is unsecure and "
957 "somebody may be spying on you. If you have no good reason to disable " 1008 "somebody may be spying on you. If you have no good reason to disable "
958 "certificate validation, please activate \"Check certificate\" in your " 1009 'certificate validation, please activate "Check certificate" in your '
959 "settings in \"Connection\" tab.")) 1010 'settings in "Connection" tab.'
960 xml_tools.quick_note(host_app, self, msg, _("Security notice"), 1011 )
961 level = C.XMLUI_DATA_LVL_WARNING) 1012 xml_tools.quick_note(
1013 host_app, self, msg, _("Security notice"), level=C.XMLUI_DATA_LVL_WARNING
1014 )
962 1015
963 @property 1016 @property
964 def server_jid(self): 1017 def server_jid(self):
965 return jid.JID(self.jid.host) 1018 return jid.JID(self.jid.host)
966 1019
1000 lambda ret: defer.ensureDeferred(self.message_add_to_history(ret)) 1053 lambda ret: defer.ensureDeferred(self.message_add_to_history(ret))
1001 ) 1054 )
1002 post_xml_treatments.addCallback(self.message_send_to_bridge) 1055 post_xml_treatments.addCallback(self.message_send_to_bridge)
1003 1056
1004 def feedback( 1057 def feedback(
1005 self, 1058 self, to_jid: jid.JID, message: str, extra: Optional[ExtraDict] = None
1006 to_jid: jid.JID,
1007 message: str,
1008 extra: Optional[ExtraDict] = None
1009 ) -> None: 1059 ) -> None:
1010 """Send message to frontends 1060 """Send message to frontends
1011 1061
1012 This message will be an info message, not recorded in history. 1062 This message will be an info message, not recorded in history.
1013 It can be used to give feedback of a command 1063 It can be used to give feedback of a command
1043 An entry point plugin is launched after component is connected. 1093 An entry point plugin is launched after component is connected.
1044 Component need to instantiate MessageProtocol itself 1094 Component need to instantiate MessageProtocol itself
1045 """ 1095 """
1046 1096
1047 trigger_suffix = ( 1097 trigger_suffix = (
1048 "Component" 1098 "Component" # used for to distinguish some trigger points set in SatXMPPEntity
1049 ) # used for to distinguish some trigger points set in SatXMPPEntity 1099 )
1050 is_component = True 1100 is_component = True
1051 # XXX: set to True from entry plugin to keep messages in history for sent messages 1101 # XXX: set to True from entry plugin to keep messages in history for sent messages
1052 sendHistory = False 1102 sendHistory = False
1053 # XXX: same as sendHistory but for received messaged 1103 # XXX: same as sendHistory but for received messaged
1054 receiveHistory = False 1104 receiveHistory = False
1055 1105
1056 def __init__(self, host_app, profile, component_jid, password, host=None, port=None, 1106 def __init__(
1057 max_retries=C.XMPP_MAX_RETRIES): 1107 self,
1108 host_app,
1109 profile,
1110 component_jid,
1111 password,
1112 host=None,
1113 port=None,
1114 max_retries=C.XMPP_MAX_RETRIES,
1115 ):
1058 self.started = time.time() 1116 self.started = time.time()
1059 if port is None: 1117 if port is None:
1060 port = C.XMPP_COMPONENT_PORT 1118 port = C.XMPP_COMPONENT_PORT
1061 1119
1062 ## entry point ## 1120 ## entry point ##
1176 plugin to your dependencies. 1234 plugin to your dependencies.
1177 A "user" part must be present in "to_jid" (otherwise, the component itself is addressed) 1235 A "user" part must be present in "to_jid" (otherwise, the component itself is addressed)
1178 @param to_jid: destination JID of the request 1236 @param to_jid: destination JID of the request
1179 """ 1237 """
1180 try: 1238 try:
1181 unescape = self.host_app.plugins['XEP-0106'].unescape 1239 unescape = self.host_app.plugins["XEP-0106"].unescape
1182 except KeyError: 1240 except KeyError:
1183 raise exceptions.MissingPlugin("Plugin XEP-0106 is needed to retrieve owner") 1241 raise exceptions.MissingPlugin("Plugin XEP-0106 is needed to retrieve owner")
1184 else: 1242 else:
1185 user = unescape(to_jid.user) 1243 user = unescape(to_jid.user)
1186 if '@' in user: 1244 if "@" in user:
1187 # a full jid is specified 1245 # a full jid is specified
1188 return jid.JID(user) 1246 return jid.JID(user)
1189 else: 1247 else:
1190 # only user part is specified, we use our own host to build the full jid 1248 # only user part is specified, we use our own host to build the full jid
1191 return jid.JID(None, (user, self.host, None)) 1249 return jid.JID(None, (user, self.host, None))
1197 with our host. 1255 with our host.
1198 Peer jid is the requesting jid from the IQ element 1256 Peer jid is the requesting jid from the IQ element
1199 @param iq_elt: IQ stanza sent from the requested 1257 @param iq_elt: IQ stanza sent from the requested
1200 @return: owner and peer JIDs 1258 @return: owner and peer JIDs
1201 """ 1259 """
1202 to_jid = jid.JID(iq_elt['to']) 1260 to_jid = jid.JID(iq_elt["to"])
1203 if to_jid.user: 1261 if to_jid.user:
1204 owner = self.get_owner_from_jid(to_jid) 1262 owner = self.get_owner_from_jid(to_jid)
1205 else: 1263 else:
1206 owner = jid.JID(iq_elt["from"]).userhostJID() 1264 owner = jid.JID(iq_elt["from"]).userhostJID()
1207 1265
1225 class SatMessageProtocol(xmppim.MessageProtocol): 1283 class SatMessageProtocol(xmppim.MessageProtocol):
1226 1284
1227 def __init__(self, host): 1285 def __init__(self, host):
1228 xmppim.MessageProtocol.__init__(self) 1286 xmppim.MessageProtocol.__init__(self)
1229 self.host = host 1287 self.host = host
1230 self.messages_queue = defer.DeferredQueue() 1288 self.messages_queue = defer.DeferredQueue()
1231 1289
1232 def setHandlerParent(self, parent): 1290 def setHandlerParent(self, parent):
1233 super().setHandlerParent(parent) 1291 super().setHandlerParent(parent)
1234 defer.ensureDeferred(self.process_messages()) 1292 defer.ensureDeferred(self.process_messages())
1235 1293
1250 @param client(SatXMPPClient, None): client to map message id to uid 1308 @param client(SatXMPPClient, None): client to map message id to uid
1251 if None, mapping will not be done 1309 if None, mapping will not be done
1252 @return(dict): message data 1310 @return(dict): message data
1253 """ 1311 """
1254 if message_elt.name != "message": 1312 if message_elt.name != "message":
1255 log.warning(_( 1313 log.warning(
1256 "parse_message used with a non <message/> stanza, ignoring: {xml}" 1314 _(
1257 .format(xml=message_elt.toXml()))) 1315 "parse_message used with a non <message/> stanza, ignoring: {xml}".format(
1316 xml=message_elt.toXml()
1317 )
1318 )
1319 )
1258 return {} 1320 return {}
1259 1321
1260 if message_elt.uri == None: 1322 if message_elt.uri == None:
1261 # xmlns may be None when wokkel element parsing strip out root namespace 1323 # xmlns may be None when wokkel element parsing strip out root namespace
1262 self.normalize_ns(message_elt, None) 1324 self.normalize_ns(message_elt, None)
1263 elif message_elt.uri != C.NS_CLIENT: 1325 elif message_elt.uri != C.NS_CLIENT:
1264 log.warning(_( 1326 log.warning(
1265 "received <message> with a wrong namespace: {xml}" 1327 _(
1266 .format(xml=message_elt.toXml()))) 1328 "received <message> with a wrong namespace: {xml}".format(
1329 xml=message_elt.toXml()
1330 )
1331 )
1332 )
1267 1333
1268 client = self.parent 1334 client = self.parent
1269 1335
1270 if not message_elt.hasAttribute('to'): 1336 if not message_elt.hasAttribute("to"):
1271 message_elt['to'] = client.jid.full() 1337 message_elt["to"] = client.jid.full()
1272 1338
1273 message = {} 1339 message = {}
1274 subject = {} 1340 subject = {}
1275 extra = {} 1341 extra = {}
1276 data: MessageData = { 1342 data: MessageData = {
1304 try: 1370 try:
1305 received_timestamp = message_elt._received_timestamp 1371 received_timestamp = message_elt._received_timestamp
1306 except AttributeError: 1372 except AttributeError:
1307 # message_elt._received_timestamp should have been set in onMessage 1373 # message_elt._received_timestamp should have been set in onMessage
1308 # but if parse_message is called directly, it can be missing 1374 # but if parse_message is called directly, it can be missing
1309 log.debug("missing received timestamp for {message_elt}".format( 1375 log.debug(
1310 message_elt=message_elt)) 1376 "missing received timestamp for {message_elt}".format(
1377 message_elt=message_elt
1378 )
1379 )
1311 received_timestamp = time.time() 1380 received_timestamp = time.time()
1312 1381
1313 try: 1382 try:
1314 delay_elt = next(message_elt.elements(delay.NS_DELAY, "delay")) 1383 delay_elt = next(message_elt.elements(delay.NS_DELAY, "delay"))
1315 except StopIteration: 1384 except StopIteration:
1319 data["timestamp"] = calendar.timegm(parsed_delay.stamp.utctimetuple()) 1388 data["timestamp"] = calendar.timegm(parsed_delay.stamp.utctimetuple())
1320 data["received_timestamp"] = received_timestamp 1389 data["received_timestamp"] = received_timestamp
1321 if parsed_delay.sender: 1390 if parsed_delay.sender:
1322 data["delay_sender"] = parsed_delay.sender.full() 1391 data["delay_sender"] = parsed_delay.sender.full()
1323 1392
1324 self.host.trigger.point("message_parse", client, message_elt, data) 1393 self.host.trigger.point("message_parse", client, message_elt, data)
1325 return data 1394 return data
1326
1327 1395
1328 def onMessage(self, message_elt: domish.Element) -> None: 1396 def onMessage(self, message_elt: domish.Element) -> None:
1329 message_elt._received_timestamp = time.time() 1397 message_elt._received_timestamp = time.time()
1330 self.messages_queue.put(message_elt) 1398 self.messages_queue.put(message_elt)
1331 1399
1345 await self.process_message(client, message_elt) 1413 await self.process_message(client, message_elt)
1346 except Exception: 1414 except Exception:
1347 log.exception(f"Can't process message {message_elt.toXml()}") 1415 log.exception(f"Can't process message {message_elt.toXml()}")
1348 1416
1349 def _on_processing_timeout( 1417 def _on_processing_timeout(
1350 self, 1418 self, message_elt: domish.Element, async_point_d: defer.Deferred
1351 message_elt: domish.Element,
1352 async_point_d: defer.Deferred
1353 ) -> None: 1419 ) -> None:
1354 log.error( 1420 log.error(
1355 "Processing of following message took too long, cancelling:" 1421 "Processing of following message took too long, cancelling:"
1356 f"{message_elt.toXml()}" 1422 f"{message_elt.toXml()}"
1357 ) 1423 )
1358 async_point_d.cancel() 1424 async_point_d.cancel()
1359 1425
1360 async def process_message( 1426 async def process_message(
1361 self, 1427 self, client: SatXMPPEntity, message_elt: domish.Element
1362 client: SatXMPPEntity,
1363 message_elt: domish.Element
1364 ) -> None: 1428 ) -> None:
1365 # TODO: handle threads 1429 # TODO: handle threads
1366 if not "from" in message_elt.attributes: 1430 if not "from" in message_elt.attributes:
1367 message_elt["from"] = client.jid.host 1431 message_elt["from"] = client.jid.host
1368 log.debug(_("got message from: {from_}").format(from_=message_elt["from"])) 1432 log.debug(_("got message from: {from_}").format(from_=message_elt["from"]))
1370 # we use client namespace all the time to simplify parsing 1434 # we use client namespace all the time to simplify parsing
1371 self.normalize_ns(message_elt, component.NS_COMPONENT_ACCEPT) 1435 self.normalize_ns(message_elt, component.NS_COMPONENT_ACCEPT)
1372 1436
1373 # plugin can add their treatments to this deferred 1437 # plugin can add their treatments to this deferred
1374 post_treat = defer.Deferred() 1438 post_treat = defer.Deferred()
1375 async_point_d = defer.ensureDeferred(self.host.trigger.async_point( 1439 async_point_d = defer.ensureDeferred(
1376 "message_received", client, message_elt, post_treat 1440 self.host.trigger.async_point(
1377 )) 1441 "message_received", client, message_elt, post_treat
1442 )
1443 )
1378 # message_received triggers block the messages queue, so they must not take too 1444 # message_received triggers block the messages queue, so they must not take too
1379 # long to proceed. 1445 # long to proceed.
1380 delayed_call = reactor.callLater( 1446 delayed_call = reactor.callLater(
1381 10, 1447 10, self._on_processing_timeout, message_elt, async_point_d
1382 self._on_processing_timeout,
1383 message_elt,
1384 async_point_d
1385 ) 1448 )
1386 trigger_ret_continue = await async_point_d 1449 trigger_ret_continue = await async_point_d
1387 1450
1388 if delayed_call.active(): 1451 if delayed_call.active():
1389 delayed_call.cancel() 1452 delayed_call.cancel()
1409 except exceptions.CancelError: 1472 except exceptions.CancelError:
1410 pass 1473 pass
1411 1474
1412 def complete_attachments(self, data: MessageData) -> MessageData: 1475 def complete_attachments(self, data: MessageData) -> MessageData:
1413 """Complete missing metadata of attachments""" 1476 """Complete missing metadata of attachments"""
1414 for attachment in data['extra'].get(C.KEY_ATTACHMENTS, []): 1477 for attachment in data["extra"].get(C.KEY_ATTACHMENTS, []):
1415 if "name" not in attachment and "url" in attachment: 1478 if "name" not in attachment and "url" in attachment:
1416 name = (Path(unquote(urlparse(attachment['url']).path)).name 1479 name = (
1417 or C.FILE_DEFAULT_NAME) 1480 Path(unquote(urlparse(attachment["url"]).path)).name
1481 or C.FILE_DEFAULT_NAME
1482 )
1418 attachment["name"] = name 1483 attachment["name"] = name
1419 if ((C.KEY_ATTACHMENTS_MEDIA_TYPE not in attachment 1484 if C.KEY_ATTACHMENTS_MEDIA_TYPE not in attachment and "name" in attachment:
1420 and "name" in attachment)): 1485 media_type = mimetypes.guess_type(attachment["name"], strict=False)[0]
1421 media_type = mimetypes.guess_type(attachment['name'], strict=False)[0]
1422 if media_type: 1486 if media_type:
1423 attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] = media_type 1487 attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] = media_type
1424 return data 1488 return data
1425 1489
1426 async def add_to_history(self, data: MessageData) -> MessageData: 1490 async def add_to_history(self, data: MessageData) -> MessageData:
1430 else: 1494 else:
1431 # we need a message to store 1495 # we need a message to store
1432 if self.parent.is_message_printable(data): 1496 if self.parent.is_message_printable(data):
1433 return await self.host.memory.add_to_history(self.parent, data) 1497 return await self.host.memory.add_to_history(self.parent, data)
1434 else: 1498 else:
1435 log.debug("not storing empty message to history: {data}" 1499 log.debug(
1436 .format(data=data)) 1500 "not storing empty message to history: {data}".format(data=data)
1501 )
1437 return data 1502 return data
1438 1503
1439 def bridge_signal(self, data: MessageData) -> MessageData: 1504 def bridge_signal(self, data: MessageData) -> MessageData:
1440 """Send signal to frontends for the given message""" 1505 """Send signal to frontends for the given message"""
1441 try: 1506 try:
1457 data["type"], 1522 data["type"],
1458 data_format.serialise(data["extra"]), 1523 data_format.serialise(data["extra"]),
1459 profile=self.parent.profile, 1524 profile=self.parent.profile,
1460 ) 1525 )
1461 else: 1526 else:
1462 log.debug("Discarding bridge signal for empty message: {data}".format( 1527 log.debug(
1463 data=data)) 1528 "Discarding bridge signal for empty message: {data}".format(data=data)
1529 )
1464 return data 1530 return data
1465 1531
1466 1532
1467 class LiberviaRosterProtocol(xmppim.RosterClientProtocol): 1533 class LiberviaRosterProtocol(xmppim.RosterClientProtocol):
1468 1534
1478 return self.is_jid_in_roster(entity_jid) 1544 return self.is_jid_in_roster(entity_jid)
1479 1545
1480 @property 1546 @property
1481 def versioning(self): 1547 def versioning(self):
1482 """True if server support roster versioning""" 1548 """True if server support roster versioning"""
1483 return (NS_ROSTER_VER, 'ver') in self.parent.xmlstream.features 1549 return (NS_ROSTER_VER, "ver") in self.parent.xmlstream.features
1484 1550
1485 @property 1551 @property
1486 def roster_cache(self): 1552 def roster_cache(self):
1487 """Cache of roster from storage 1553 """Cache of roster from storage
1488 1554
1545 self._groups.clear() 1611 self._groups.clear()
1546 yield self.request_roster() 1612 yield self.request_roster()
1547 1613
1548 @defer.inlineCallbacks 1614 @defer.inlineCallbacks
1549 def request_roster(self): 1615 def request_roster(self):
1550 """Ask the server for Roster list """ 1616 """Ask the server for Roster list"""
1551 if self.versioning: 1617 if self.versioning:
1552 log.info(_("our server support roster versioning, we use it")) 1618 log.info(_("our server support roster versioning, we use it"))
1553 roster_cache = self.roster_cache 1619 roster_cache = self.roster_cache
1554 yield roster_cache.load() 1620 yield roster_cache.load()
1555 try: 1621 try:
1563 # we deserialise cached roster to our local cache 1629 # we deserialise cached roster to our local cache
1564 for roster_jid_s, roster_item_elt_s in roster_cache.items(): 1630 for roster_jid_s, roster_item_elt_s in roster_cache.items():
1565 if roster_jid_s == ROSTER_VER_KEY: 1631 if roster_jid_s == ROSTER_VER_KEY:
1566 continue 1632 continue
1567 roster_jid = jid.JID(roster_jid_s) 1633 roster_jid = jid.JID(roster_jid_s)
1568 roster_item_elt = generic.parseXml(roster_item_elt_s.encode('utf-8')) 1634 roster_item_elt = generic.parseXml(roster_item_elt_s.encode("utf-8"))
1569 roster_item = xmppim.RosterItem.fromElement(roster_item_elt) 1635 roster_item = xmppim.RosterItem.fromElement(roster_item_elt)
1570 self._jids[roster_jid] = roster_item 1636 self._jids[roster_jid] = roster_item
1571 self._register_item(roster_item) 1637 self._register_item(roster_item)
1572 else: 1638 else:
1573 log.warning(_("our server doesn't support roster versioning")) 1639 log.warning(_("our server doesn't support roster versioning"))
1574 version = None 1640 version = None
1575 1641
1576 log.debug("requesting roster") 1642 log.debug("requesting roster")
1577 roster = yield self.getRoster(version=version) 1643 roster = yield self.getRoster(version=version)
1578 if roster is None: 1644 if roster is None:
1579 log.debug("empty roster result received, we'll get roster item with roster " 1645 log.debug(
1580 "pushes") 1646 "empty roster result received, we'll get roster item with roster "
1647 "pushes"
1648 )
1581 else: 1649 else:
1582 # a full roster is received 1650 # a full roster is received
1583 self._groups.clear() 1651 self._groups.clear()
1584 self._jids = roster 1652 self._jids = roster
1585 for item in roster.values(): 1653 for item in roster.values():
1587 # XXX: current behaviour: we don't want contact in our roster list 1655 # XXX: current behaviour: we don't want contact in our roster list
1588 # if there is no presence subscription 1656 # if there is no presence subscription
1589 # may change in the future 1657 # may change in the future
1590 log.info( 1658 log.info(
1591 "Removing contact {} from roster because there is no presence " 1659 "Removing contact {} from roster because there is no presence "
1592 "subscription".format( 1660 "subscription".format(item.jid)
1593 item.jid
1594 )
1595 ) 1661 )
1596 self.removeItem(item.entity) # FIXME: to be checked 1662 self.removeItem(item.entity) # FIXME: to be checked
1597 else: 1663 else:
1598 self._register_item(item) 1664 self._register_item(item)
1599 yield self._cache_roster(roster.version) 1665 yield self._cache_roster(roster.version)
1644 except KeyError: 1710 except KeyError:
1645 pass # no previous item registration (or it's been cleared) 1711 pass # no previous item registration (or it's been cleared)
1646 self._jids[entity] = item 1712 self._jids[entity] = item
1647 self._register_item(item) 1713 self._register_item(item)
1648 self.host.bridge.contact_new( 1714 self.host.bridge.contact_new(
1649 entity.full(), self.get_attributes(item), list(item.groups), 1715 entity.full(),
1650 self.parent.profile 1716 self.get_attributes(item),
1717 list(item.groups),
1718 self.parent.profile,
1651 ) 1719 )
1652 1720
1653 def removeReceived(self, request): 1721 def removeReceived(self, request):
1654 entity = request.item.entity 1722 entity = request.item.entity
1655 log.info(_("removing {entity} from roster").format(entity=entity.full())) 1723 log.info(_("removing {entity} from roster").format(entity=entity.full()))
1708 1776
1709 def is_jid_in_roster(self, entity_jid): 1777 def is_jid_in_roster(self, entity_jid):
1710 """Return True if jid is in roster""" 1778 """Return True if jid is in roster"""
1711 if not isinstance(entity_jid, jid.JID): 1779 if not isinstance(entity_jid, jid.JID):
1712 raise exceptions.InternalError( 1780 raise exceptions.InternalError(
1713 f"a JID is expected, not {type(entity_jid)}: {entity_jid!r}") 1781 f"a JID is expected, not {type(entity_jid)}: {entity_jid!r}"
1782 )
1714 return entity_jid in self._jids 1783 return entity_jid in self._jids
1715 1784
1716 def is_subscribed_from(self, entity_jid: jid.JID) -> bool: 1785 def is_subscribed_from(self, entity_jid: jid.JID) -> bool:
1717 """Return True if entity is authorised to see our presence""" 1786 """Return True if entity is authorised to see our presence"""
1718 try: 1787 try:
1823 1892
1824 if None in statuses: # we only want string keys 1893 if None in statuses: # we only want string keys
1825 statuses[C.PRESENCE_STATUSES_DEFAULT] = statuses.pop(None) 1894 statuses[C.PRESENCE_STATUSES_DEFAULT] = statuses.pop(None)
1826 1895
1827 if not self.host.trigger.point( 1896 if not self.host.trigger.point(
1828 "presence_received", self.parent, entity, C.PRESENCE_UNAVAILABLE, 0, statuses, 1897 "presence_received",
1898 self.parent,
1899 entity,
1900 C.PRESENCE_UNAVAILABLE,
1901 0,
1902 statuses,
1829 ): 1903 ):
1830 return 1904 return
1831 1905
1832 # now it's time to notify frontends 1906 # now it's time to notify frontends
1833 # if the entity is not known yet in this session or is already unavailable, 1907 # if the entity is not known yet in this session or is already unavailable,
1834 # there is no need to send an unavailable signal 1908 # there is no need to send an unavailable signal
1835 try: 1909 try:
1836 presence = self.host.memory.get_entity_datum( 1910 presence = self.host.memory.get_entity_datum(self.client, entity, "presence")
1837 self.client, entity, "presence"
1838 )
1839 except (KeyError, exceptions.UnknownEntityError): 1911 except (KeyError, exceptions.UnknownEntityError):
1840 # the entity has not been seen yet in this session 1912 # the entity has not been seen yet in this session
1841 pass 1913 pass
1842 else: 1914 else:
1843 if presence.show != C.PRESENCE_UNAVAILABLE: 1915 if presence.show != C.PRESENCE_UNAVAILABLE:
1949 disco.DiscoClientProtocol.__init__(self) 2021 disco.DiscoClientProtocol.__init__(self)
1950 2022
1951 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): 2023 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
1952 # those features are implemented in Wokkel (or sat_tmp.wokkel) 2024 # those features are implemented in Wokkel (or sat_tmp.wokkel)
1953 # and thus are always available 2025 # and thus are always available
1954 return [disco.DiscoFeature(NS_X_DATA), 2026 return [
1955 disco.DiscoFeature(NS_XML_ELEMENT), 2027 disco.DiscoFeature(NS_X_DATA),
1956 disco.DiscoFeature(NS_DISCO_INFO)] 2028 disco.DiscoFeature(NS_XML_ELEMENT),
2029 disco.DiscoFeature(NS_DISCO_INFO),
2030 ]
1957 2031
1958 def getDiscoItems(self, requestor, target, nodeIdentifier=""): 2032 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
1959 return [] 2033 return []
1960 2034
1961 2035
1983 2057
1984 2058
1985 @implementer(iwokkel.IDisco) 2059 @implementer(iwokkel.IDisco)
1986 class SatIdentityHandler(XMPPHandler): 2060 class SatIdentityHandler(XMPPHandler):
1987 """Manage disco Identity of SàT.""" 2061 """Manage disco Identity of SàT."""
2062
1988 # TODO: dynamic identity update (see docstring). Note that a XMPP entity can have 2063 # TODO: dynamic identity update (see docstring). Note that a XMPP entity can have
1989 # several identities 2064 # several identities
1990 2065
1991 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): 2066 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
1992 return self.parent.identities 2067 return self.parent.identities