1## protocol.py 2## 3## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov 4## 5## This program is free software; you can redistribute it and/or modify 6## it under the terms of the GNU General Public License as published by 7## the Free Software Foundation; either version 2, or (at your option) 8## any later version. 9## 10## This program is distributed in the hope that it will be useful, 11## but WITHOUT ANY WARRANTY; without even the implied warranty of 12## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13## GNU General Public License for more details. 14 15""" 16Protocol module contains tools that are needed for processing of xmpp-related 17data structures, including jabber-objects like JID or different stanzas and 18sub- stanzas) handling routines 19""" 20 21import time 22import hashlib 23import functools 24import warnings 25from base64 import b64encode 26from collections import namedtuple 27 28from gi.repository import GLib 29 30import idna 31from precis_i18n import get_profile 32from nbxmpp.simplexml import Node 33from nbxmpp.namespaces import Namespace 34 35def ascii_upper(s): 36 return s.upper() 37 38SASL_AUTH_MECHS = [ 39 'SCRAM-SHA-256-PLUS', 40 'SCRAM-SHA-256', 41 'SCRAM-SHA-1-PLUS', 42 'SCRAM-SHA-1', 43 'GSSAPI', 44 'PLAIN', 45 'EXTERNAL', 46 'ANONYMOUS', 47] 48 49SASL_ERROR_CONDITIONS = [ 50 'aborted', 51 'account-disabled', 52 'credentials-expired', 53 'encryption-required', 54 'incorrect-encoding', 55 'invalid-authzid', 56 'invalid-mechanism', 57 'mechanism-too-weak', 58 'malformed-request', 59 'not-authorized', 60 'temporary-auth-failure', 61] 62 63ERRORS = { 64 'urn:ietf:params:xml:ns:xmpp-sasl aborted': ['', 65 '', 66 'The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element.'], 67 'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding': ['', 68 '', 69 'The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data.'], 70 'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid': ['', 71 '', 72 'The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data.'], 73 'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism': ['', 74 '', 75 'The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element.'], 76 'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak': ['', 77 '', 78 'The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data.'], 79 'urn:ietf:params:xml:ns:xmpp-sasl not-authorized': ['', 80 '', 81 'The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data.'], 82 'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure': ['', 83 '', 84 'The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element.'], 85 'urn:ietf:params:xml:ns:xmpp-stanzas bad-request': ['400', 86 'modify', 87 'The sender has sent XML that is malformed or that cannot be processed.'], 88 'urn:ietf:params:xml:ns:xmpp-stanzas conflict': ['409', 89 'cancel', 90 'Access cannot be granted because an existing resource or session exists with the same name or address.'], 91 'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented': ['501', 92 'cancel', 93 'The feature requested is not implemented by the recipient or server and therefore cannot be processed.'], 94 'urn:ietf:params:xml:ns:xmpp-stanzas forbidden': ['403', 95 'auth', 96 'The requesting entity does not possess the required permissions to perform the action.'], 97 'urn:ietf:params:xml:ns:xmpp-stanzas gone': ['302', 98 'modify', 99 'The recipient or server can no longer be contacted at this address.'], 100 'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error': ['500', 101 'wait', 102 'The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.'], 103 'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found': ['404', 104 'cancel', 105 'The addressed JID or item requested cannot be found.'], 106 'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed': ['400', 107 'modify', 108 "The value of the 'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme."], 109 'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable': ['406', 110 'cancel', 111 'The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server.'], 112 'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed': ['405', 113 'cancel', 114 'The recipient or server does not allow any entity to perform the action.'], 115 'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized': ['401', 116 'auth', 117 'The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.'], 118 'urn:ietf:params:xml:ns:xmpp-stanzas payment-required': ['402', 119 'auth', 120 'The requesting entity is not authorized to access the requested service because payment is required.'], 121 'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable': ['404', 122 'wait', 123 'The intended recipient is temporarily unavailable.'], 124 'urn:ietf:params:xml:ns:xmpp-stanzas redirect': ['302', 125 'modify', 126 'The recipient or server is redirecting requests for this information to another entity.'], 127 'urn:ietf:params:xml:ns:xmpp-stanzas registration-required': ['407', 128 'auth', 129 'The requesting entity is not authorized to access the requested service because registration is required.'], 130 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found': ['404', 131 'cancel', 132 'A remote server or service specified as part or all of the JID of the intended recipient does not exist.'], 133 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout': ['504', 134 'wait', 135 'A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time.'], 136 'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint': ['500', 137 'wait', 138 'The server or recipient lacks the system resources necessary to service the request.'], 139 'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable': ['503', 140 'cancel', 141 'The server or recipient does not currently provide the requested service.'], 142 'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required': ['407', 143 'auth', 144 'The requesting entity is not authorized to access the requested service because a subscription is required.'], 145 'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition': ['500', 146 '', 147 'Undefined Condition'], 148 'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request': ['400', 149 'wait', 150 'The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).'], 151 'urn:ietf:params:xml:ns:xmpp-streams bad-format': ['', 152 '', 153 'The entity has sent XML that cannot be processed.'], 154 'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix': ['', 155 '', 156 'The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.'], 157 'urn:ietf:params:xml:ns:xmpp-streams conflict': ['', 158 '', 159 'The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.'], 160 'urn:ietf:params:xml:ns:xmpp-streams connection-timeout': ['', 161 '', 162 'The entity has not generated any traffic over the stream for some period of time.'], 163 'urn:ietf:params:xml:ns:xmpp-streams host-gone': ['', 164 '', 165 "The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server."], 166 'urn:ietf:params:xml:ns:xmpp-streams host-unknown': ['', 167 '', 168 "The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server."], 169 'urn:ietf:params:xml:ns:xmpp-streams improper-addressing': ['', 170 '', 171 "A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value)."], 172 'urn:ietf:params:xml:ns:xmpp-streams internal-server-error': ['', 173 '', 174 'The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.'], 175 'urn:ietf:params:xml:ns:xmpp-streams invalid-from': ['cancel', 176 '', 177 "The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization."], 178 'urn:ietf:params:xml:ns:xmpp-streams invalid-id': ['', 179 '', 180 'The stream ID or dialback ID is invalid or does not match an ID previously provided.'], 181 'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace': ['', 182 '', 183 'The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something other than "jabber:server:dialback".'], 184 'urn:ietf:params:xml:ns:xmpp-streams invalid-xml': ['', 185 '', 186 'The entity has sent invalid XML over the stream to a server that performs validation.'], 187 'urn:ietf:params:xml:ns:xmpp-streams not-authorized': ['', 188 '', 189 'The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation.'], 190 'urn:ietf:params:xml:ns:xmpp-streams policy-violation': ['', 191 '', 192 'The entity has violated some local service policy.'], 193 'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed': ['', 194 '', 195 'The server is unable to properly connect to a remote resource that is required for authentication or authorization.'], 196 'urn:ietf:params:xml:ns:xmpp-streams resource-constraint': ['', 197 '', 198 'The server lacks the system resources necessary to service the stream.'], 199 'urn:ietf:params:xml:ns:xmpp-streams restricted-xml': ['', 200 '', 201 'The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.'], 202 'urn:ietf:params:xml:ns:xmpp-streams see-other-host': ['', 203 '', 204 'The server will not provide service to the initiating entity but is redirecting traffic to another host.'], 205 'urn:ietf:params:xml:ns:xmpp-streams system-shutdown': ['', 206 '', 207 'The server is being shut down and all active streams are being closed.'], 208 'urn:ietf:params:xml:ns:xmpp-streams undefined-condition': ['', 209 '', 210 'The error condition is not one of those defined by the other conditions in this list.'], 211 'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding': ['', 212 '', 213 'The initiating entity has encoded the stream in an encoding that is not supported by the server.'], 214 'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type': ['', 215 '', 216 'The initiating entity has sent a first-level child of the stream that is not supported by the server.'], 217 'urn:ietf:params:xml:ns:xmpp-streams unsupported-version': ['', 218 '', 219 "The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server."], 220 'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed': ['', 221 '', 222 'The initiating entity has sent XML that is not well-formed.'] 223} 224 225_errorcodes = { 226 '302': 'redirect', 227 '400': 'unexpected-request', 228 '401': 'not-authorized', 229 '402': 'payment-required', 230 '403': 'forbidden', 231 '404': 'remote-server-not-found', 232 '405': 'not-allowed', 233 '406': 'not-acceptable', 234 '407': 'subscription-required', 235 '409': 'conflict', 236 '500': 'undefined-condition', 237 '501': 'feature-not-implemented', 238 '503': 'service-unavailable', 239 '504': 'remote-server-timeout', 240 'cancel': 'invalid-from' 241} 242 243_status_conditions = { 244 'realjid-public': 100, 245 'affiliation-changed': 101, 246 'unavailable-shown': 102, 247 'unavailable-not-shown': 103, 248 'configuration-changed': 104, 249 'self-presence': 110, 250 'logging-enabled': 170, 251 'logging-disabled': 171, 252 'non-anonymous': 172, 253 'semi-anonymous': 173, 254 'fully-anonymous': 174, 255 'room-created': 201, 256 'nick-assigned': 210, 257 'banned': 301, 258 'new-nick': 303, 259 'kicked': 307, 260 'removed-affiliation': 321, 261 'removed-membership': 322, 262 'removed-shutdown': 332, 263} 264 265_localpart_disallowed_chars = set('"&\'/:<>@') 266_localpart_escape_chars = ' "&\'/:<>@' 267 268 269STREAM_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-streams not-authorized' 270STREAM_REMOTE_CONNECTION_FAILED = 'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed' 271SASL_MECHANISM_TOO_WEAK = 'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak' 272STREAM_XML_NOT_WELL_FORMED = 'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed' 273ERR_JID_MALFORMED = 'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed' 274STREAM_SEE_OTHER_HOST = 'urn:ietf:params:xml:ns:xmpp-streams see-other-host' 275STREAM_BAD_NAMESPACE_PREFIX = 'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix' 276ERR_SERVICE_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable' 277STREAM_CONNECTION_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-streams connection-timeout' 278STREAM_UNSUPPORTED_VERSION = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-version' 279STREAM_IMPROPER_ADDRESSING = 'urn:ietf:params:xml:ns:xmpp-streams improper-addressing' 280STREAM_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-streams undefined-condition' 281SASL_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-sasl not-authorized' 282ERR_GONE = 'urn:ietf:params:xml:ns:xmpp-stanzas gone' 283SASL_TEMPORARY_AUTH_FAILURE = 'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure' 284ERR_REMOTE_SERVER_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found' 285ERR_UNEXPECTED_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request' 286ERR_RECIPIENT_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable' 287ERR_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-stanzas conflict' 288STREAM_SYSTEM_SHUTDOWN = 'urn:ietf:params:xml:ns:xmpp-streams system-shutdown' 289STREAM_BAD_FORMAT = 'urn:ietf:params:xml:ns:xmpp-streams bad-format' 290ERR_SUBSCRIPTION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required' 291STREAM_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-streams internal-server-error' 292ERR_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized' 293SASL_ABORTED = 'urn:ietf:params:xml:ns:xmpp-sasl aborted' 294ERR_REGISTRATION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas registration-required' 295ERR_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error' 296SASL_INCORRECT_ENCODING = 'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding' 297STREAM_HOST_GONE = 'urn:ietf:params:xml:ns:xmpp-streams host-gone' 298STREAM_POLICY_VIOLATION = 'urn:ietf:params:xml:ns:xmpp-streams policy-violation' 299STREAM_INVALID_XML = 'urn:ietf:params:xml:ns:xmpp-streams invalid-xml' 300STREAM_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-streams conflict' 301STREAM_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-streams resource-constraint' 302STREAM_UNSUPPORTED_ENCODING = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding' 303ERR_NOT_ALLOWED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed' 304ERR_ITEM_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found' 305ERR_NOT_ACCEPTABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable' 306STREAM_INVALID_FROM = 'urn:ietf:params:xml:ns:xmpp-streams invalid-from' 307ERR_FEATURE_NOT_IMPLEMENTED = 'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented' 308ERR_BAD_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas bad-request' 309STREAM_INVALID_ID = 'urn:ietf:params:xml:ns:xmpp-streams invalid-id' 310STREAM_HOST_UNKNOWN = 'urn:ietf:params:xml:ns:xmpp-streams host-unknown' 311ERR_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition' 312SASL_INVALID_MECHANISM = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism' 313STREAM_RESTRICTED_XML = 'urn:ietf:params:xml:ns:xmpp-streams restricted-xml' 314ERR_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint' 315ERR_REMOTE_SERVER_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout' 316SASL_INVALID_AUTHZID = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid' 317ERR_PAYMENT_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas payment-required' 318STREAM_INVALID_NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace' 319ERR_REDIRECT = 'urn:ietf:params:xml:ns:xmpp-stanzas redirect' 320STREAM_UNSUPPORTED_STANZA_TYPE = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type' 321ERR_FORBIDDEN = 'urn:ietf:params:xml:ns:xmpp-stanzas forbidden' 322 323def isResultNode(node): 324 """ 325 Return true if the node is a positive reply 326 """ 327 return node and node.getType() == 'result' 328 329def isErrorNode(node): 330 """ 331 Return true if the node is a negative reply 332 """ 333 return node and node.getType() == 'error' 334 335def isMucPM(message): 336 muc_user = message.getTag('x', namespace=Namespace.MUC_USER) 337 return (message.getType() in ('chat', 'error') and 338 muc_user is not None and 339 not muc_user.getChildren()) 340 341class NodeProcessed(Exception): 342 """ 343 Exception that should be raised by handler when the handling should be 344 stopped 345 """ 346 347class StreamError(Exception): 348 """ 349 Base exception class for stream errors 350 """ 351 352class BadFormat(StreamError): 353 pass 354 355class BadNamespacePrefix(StreamError): 356 pass 357 358class Conflict(StreamError): 359 pass 360 361class ConnectionTimeout(StreamError): 362 pass 363 364class HostGone(StreamError): 365 pass 366 367class HostUnknown(StreamError): 368 pass 369 370class ImproperAddressing(StreamError): 371 pass 372 373class InternalServerError(StreamError): 374 pass 375 376class InvalidFrom(StreamError): 377 pass 378 379class InvalidID(StreamError): 380 pass 381 382class InvalidNamespace(StreamError): 383 pass 384 385class InvalidXML(StreamError): 386 pass 387 388class NotAuthorized(StreamError): 389 pass 390 391class PolicyViolation(StreamError): 392 pass 393 394class RemoteConnectionFailed(StreamError): 395 pass 396 397class ResourceConstraint(StreamError): 398 pass 399 400class RestrictedXML(StreamError): 401 pass 402 403class SeeOtherHost(StreamError): 404 pass 405 406class SystemShutdown(StreamError): 407 pass 408 409class UndefinedCondition(StreamError): 410 pass 411 412class UnsupportedEncoding(StreamError): 413 pass 414 415class UnsupportedStanzaType(StreamError): 416 pass 417 418class UnsupportedVersion(StreamError): 419 pass 420 421class XMLNotWellFormed(StreamError): 422 pass 423 424class InvalidStanza(Exception): 425 pass 426 427class InvalidJid(Exception): 428 pass 429 430class LocalpartByteLimit(InvalidJid): 431 def __init__(self): 432 InvalidJid.__init__(self, 'Localpart must be between 1 and 1023 bytes') 433 434class LocalpartNotAllowedChar(InvalidJid): 435 def __init__(self): 436 InvalidJid.__init__(self, 'Not allowed character in localpart') 437 438class ResourcepartByteLimit(InvalidJid): 439 def __init__(self): 440 InvalidJid.__init__(self, 441 'Resourcepart must be between 1 and 1023 bytes') 442 443class ResourcepartNotAllowedChar(InvalidJid): 444 def __init__(self): 445 InvalidJid.__init__(self, 'Not allowed character in resourcepart') 446 447class DomainpartByteLimit(InvalidJid): 448 def __init__(self): 449 InvalidJid.__init__(self, 'Domainpart must be between 1 and 1023 bytes') 450 451class DomainpartNotAllowedChar(InvalidJid): 452 def __init__(self): 453 InvalidJid.__init__(self, 'Not allowed character in domainpart') 454 455class StanzaMalformed(Exception): 456 pass 457 458class DiscoInfoMalformed(Exception): 459 pass 460 461stream_exceptions = {'bad-format': BadFormat, 462 'bad-namespace-prefix': BadNamespacePrefix, 463 'conflict': Conflict, 464 'connection-timeout': ConnectionTimeout, 465 'host-gone': HostGone, 466 'host-unknown': HostUnknown, 467 'improper-addressing': ImproperAddressing, 468 'internal-server-error': InternalServerError, 469 'invalid-from': InvalidFrom, 470 'invalid-id': InvalidID, 471 'invalid-namespace': InvalidNamespace, 472 'invalid-xml': InvalidXML, 473 'not-authorized': NotAuthorized, 474 'policy-violation': PolicyViolation, 475 'remote-connection-failed': RemoteConnectionFailed, 476 'resource-constraint': ResourceConstraint, 477 'restricted-xml': RestrictedXML, 478 'see-other-host': SeeOtherHost, 479 'system-shutdown': SystemShutdown, 480 'undefined-condition': UndefinedCondition, 481 'unsupported-encoding': UnsupportedEncoding, 482 'unsupported-stanza-type': UnsupportedStanzaType, 483 'unsupported-version': UnsupportedVersion, 484 'xml-not-well-formed': XMLNotWellFormed} 485 486 487def deprecation_warning(message): 488 warnings.warn(message, DeprecationWarning) 489 490 491@functools.lru_cache(maxsize=None) 492def validate_localpart(localpart): 493 if not localpart or len(localpart.encode()) > 1023: 494 raise LocalpartByteLimit 495 496 if _localpart_disallowed_chars & set(localpart): 497 raise LocalpartNotAllowedChar 498 499 try: 500 username = get_profile('UsernameCaseMapped') 501 return username.enforce(localpart) 502 except Exception: 503 raise LocalpartNotAllowedChar 504 505 506@functools.lru_cache(maxsize=None) 507def validate_resourcepart(resourcepart): 508 if not resourcepart or len(resourcepart.encode()) > 1023: 509 raise ResourcepartByteLimit 510 511 try: 512 opaque = get_profile('OpaqueString') 513 return opaque.enforce(resourcepart) 514 except Exception: 515 raise ResourcepartNotAllowedChar 516 517 518@functools.lru_cache(maxsize=None) 519def validate_domainpart(domainpart): 520 if not domainpart: 521 raise DomainpartByteLimit 522 523 ip_address = domainpart.strip('[]') 524 if GLib.hostname_is_ip_address(ip_address): 525 return ip_address 526 527 length = len(domainpart.encode()) 528 if length == 0 or length > 1023: 529 raise DomainpartByteLimit 530 531 if domainpart.endswith('.'): # RFC7622, 3.2 532 domainpart = domainpart[:-1] 533 534 try: 535 idna_encode(domainpart) 536 except Exception: 537 raise DomainpartNotAllowedChar 538 539 return domainpart 540 541 542@functools.lru_cache(maxsize=None) 543def idna_encode(domain): 544 return idna.encode(domain, uts46=True).decode() 545 546 547@functools.lru_cache(maxsize=None) 548def escape_localpart(localpart): 549 # https://xmpp.org/extensions/xep-0106.html#bizrules-algorithm 550 # 551 # If there are any instances of character sequences that correspond 552 # to escapings of the disallowed characters 553 # (e.g., the character sequence "\27") or the escaping character 554 # (i.e., the character sequence "\5c") in the source address, 555 # the leading backslash character MUST be escaped to the character 556 # sequence "\5c" 557 558 for char in '\\' + _localpart_escape_chars: 559 seq = "\\{:02x}".format(ord(char)) 560 localpart = localpart.replace(seq, "\\5c{:02x}".format(ord(char))) 561 562 # Escape all other chars 563 for char in _localpart_escape_chars: 564 localpart = localpart.replace(char, "\\{:02x}".format(ord(char))) 565 566 return localpart 567 568 569@functools.lru_cache(maxsize=None) 570def unescape_localpart(localpart): 571 if localpart.startswith('\\20') or localpart.endswith('\\20'): 572 # Escaped JIDs are not allowed to start or end with \20 573 # so this localpart must be already unescaped 574 return localpart 575 576 for char in _localpart_escape_chars: 577 seq = "\\{:02x}".format(ord(char)) 578 localpart = localpart.replace(seq, char) 579 580 for char in _localpart_escape_chars + "\\": 581 seq = "\\5c{:02x}".format(ord(char)) 582 localpart = localpart.replace(seq, "\\{:02x}".format(ord(char))) 583 584 return localpart 585 586 587class JID(namedtuple('JID', 588 ['jid', 'localpart', 'domain', 'resource'])): 589 590 __slots__ = [] 591 592 def __new__(cls, jid=None, localpart=None, domain=None, resource=None): 593 if jid is not None: 594 deprecation_warning('JID(jid) is deprecated, use from_string()') 595 return JID.from_string(str(jid)) 596 597 if localpart is not None: 598 localpart = validate_localpart(localpart) 599 600 domain = validate_domainpart(domain) 601 602 if resource is not None: 603 resource = validate_resourcepart(resource) 604 605 return super().__new__(cls, None, localpart, domain, resource) 606 607 @classmethod 608 @functools.lru_cache(maxsize=None) 609 def from_string(cls, jid_string): 610 # https://tools.ietf.org/html/rfc7622#section-3.2 611 612 # Remove any portion from the first '/' character to the end of the 613 # string (if there is a '/' character present). 614 615 # Remove any portion from the beginning of the string to the first 616 # '@' character (if there is an '@' character present). 617 618 if jid_string.find('/') != -1: 619 rest, resourcepart = jid_string.split('/', 1) 620 else: 621 rest, resourcepart = jid_string, None 622 623 if rest.find('@') != -1: 624 localpart, domainpart = rest.split('@', 1) 625 else: 626 localpart, domainpart = None, rest 627 628 return cls(jid=None, 629 localpart=localpart, 630 domain=domainpart, 631 resource=resourcepart) 632 633 @classmethod 634 @functools.lru_cache(maxsize=None) 635 def from_user_input(cls, user_input, escaped=False): 636 # Use this if we want JIDs to be escaped according to XEP-0106 637 # The standard JID parsing cannot be applied because user_input is 638 # not a valid JID. 639 640 # Only user_input which after escaping result in a bare JID can be 641 # successfully parsed. 642 643 # The assumpution is user_input is a bare JID so we start with an 644 # rsplit on @ because we assume there is no resource, so the char @ 645 # in the localpart can later be escaped. 646 647 if escaped: 648 # for convenience 649 return cls.from_string(user_input) 650 651 if '@' in user_input: 652 localpart, domainpart = user_input.rsplit('@', 1) 653 if localpart.startswith(' ') or localpart.endswith(' '): 654 raise LocalpartNotAllowedChar 655 656 localpart = escape_localpart(localpart) 657 658 else: 659 localpart = None 660 domainpart = user_input 661 662 return cls(jid=None, 663 localpart=localpart, 664 domain=domainpart, 665 resource=None) 666 667 def __str__(self): 668 if self.localpart: 669 jid = f'{self.localpart}@{self.domain}' 670 else: 671 jid = self.domain 672 673 if self.resource is not None: 674 return f'{jid}/{self.resource}' 675 return jid 676 677 def __hash__(self): 678 return hash(str(self)) 679 680 def __eq__(self, other): 681 if isinstance(other, str): 682 deprecation_warning('comparing string with JID is deprected') 683 try: 684 return JID.from_string(other) == self 685 except Exception: 686 return False 687 return super().__eq__(other) 688 689 def __ne__(self, other): 690 return not self.__eq__(other) 691 692 def domain_to_ascii(self): 693 return idna_encode(self.domain) 694 695 @property 696 def bare(self): 697 if self.localpart is not None: 698 return f'{self.localpart}@{self.domain}' 699 return self.domain 700 701 @property 702 def is_bare(self): 703 return self.resource is None 704 705 def new_as_bare(self): 706 if self.resource is None: 707 return self 708 return self._replace(resource=None) 709 710 def bare_match(self, other): 711 if isinstance(other, str): 712 other = JID.from_string(other) 713 return self.bare == other.bare 714 715 @property 716 def is_domain(self): 717 return self.localpart is None and self.resource is None 718 719 @property 720 def is_full(self): 721 return (self.localpart is not None and 722 self.domain is not None and 723 self.resource is not None) 724 725 def new_with(self, **kwargs): 726 return self._replace(**kwargs) 727 728 def to_user_string(self, show_punycode=True): 729 domain = self.domain_to_ascii() 730 if domain.startswith('xn--') and show_punycode: 731 domain_encoded = f' ({domain})' 732 else: 733 domain_encoded = '' 734 735 if self.localpart is None: 736 return f'{self}{domain_encoded}' 737 738 localpart = unescape_localpart(self.localpart) 739 740 if self.resource is None: 741 return f'{localpart}@{self.domain}{domain_encoded}' 742 return f'{localpart}@{self.domain}/{self.resource}{domain_encoded}' 743 744 def bareMatch(self, other): 745 deprecation_warning('bareMatch() is deprected use bare_match()') 746 return self.bare_match(other) 747 748 @property 749 def isBare(self): 750 deprecation_warning('isBare() is deprected use ' 751 'the attribute is_bare') 752 return self.is_bare 753 754 @property 755 def isDomain(self): 756 deprecation_warning('isDomain() is deprected use ' 757 'the attribute is_domain') 758 return self.is_domain 759 760 @property 761 def isFull(self): 762 deprecation_warning('isFull() is deprected use ' 763 'the attribute is_full') 764 return self.is_full 765 766 def copy(self): 767 deprecation_warning('copy() is not needed, JID is immutable') 768 return self 769 770 def getNode(self): 771 deprecation_warning('getNode() is deprected use ' 772 'the attribute localpart') 773 return self.localpart 774 775 def getDomain(self): 776 deprecation_warning('getDomain() is deprected use ' 777 'the attribute domain') 778 return self.domain 779 780 def getResource(self): 781 deprecation_warning('getResource() is deprected use ' 782 'the attribute resource') 783 return self.resource 784 785 def getStripped(self): 786 deprecation_warning('getStripped() is deprected use ' 787 'the attribute bare') 788 return self.bare 789 790 def getBare(self): 791 deprecation_warning('getBare() is deprected use ' 792 'the attribute bare') 793 return self.bare 794 795 796class StreamErrorNode(Node): 797 def __init__(self, node): 798 Node.__init__(self, node=node) 799 800 self._text = {} 801 802 text_elements = self.getTags('text', namespace=Namespace.XMPP_STREAMS) 803 for element in text_elements: 804 lang = element.getXmlLang() 805 text = element.getData() 806 self._text[lang] = text 807 808 def get_condition(self): 809 for tag in self.getChildren(): 810 if (tag.getName() != 'text' and 811 tag.getNamespace() == Namespace.XMPP_STREAMS): 812 return tag.getName() 813 return None 814 815 def get_text(self, pref_lang=None): 816 if pref_lang is not None: 817 text = self._text.get(pref_lang) 818 if text is not None: 819 return text 820 821 if self._text: 822 text = self._text.get('en') 823 if text is not None: 824 return text 825 826 text = self._text.get(None) 827 if text is not None: 828 return text 829 return self._text.popitem()[1] 830 return '' 831 832 833class Protocol(Node): 834 """ 835 A "stanza" object class. Contains methods that are common for presences, iqs 836 and messages 837 """ 838 839 def __init__(self, 840 name=None, 841 to=None, 842 typ=None, 843 frm=None, 844 attrs=None, 845 payload=None, 846 timestamp=None, 847 xmlns=None, 848 node=None): 849 """ 850 Constructor, name is the name of the stanza 851 i.e. 'message' or 'presence'or 'iq' 852 853 to is the value of 'to' attribure, 'typ' - 'type' attribute 854 frn - from attribure, attrs - other attributes mapping, 855 payload - same meaning as for simplexml payload definition 856 timestamp - the time value that needs to be stamped over stanza 857 xmlns - namespace of top stanza node 858 node - parsed or unparsed stana to be taken as prototype. 859 """ 860 if not attrs: 861 attrs = {} 862 if to: 863 attrs['to'] = to 864 if frm: 865 attrs['from'] = frm 866 if typ: 867 attrs['type'] = typ 868 Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node) 869 if not node and xmlns: 870 self.setNamespace(xmlns) 871 if self['to']: 872 self.setTo(self['to']) 873 if self['from']: 874 self.setFrom(self['from']) 875 if (node and 876 isinstance(node, Protocol) and 877 self.__class__ == node.__class__ 878 and 'id' in self.attrs): 879 del self.attrs['id'] 880 self.timestamp = None 881 for d in self.getTags('delay', namespace=Namespace.DELAY2): 882 try: 883 if d.getAttr('stamp') < self.getTimestamp2(): 884 self.setTimestamp(d.getAttr('stamp')) 885 except Exception: 886 pass 887 if not self.timestamp: 888 for x in self.getTags('x', namespace=Namespace.DELAY): 889 try: 890 if x.getAttr('stamp') < self.getTimestamp(): 891 self.setTimestamp(x.getAttr('stamp')) 892 except Exception: 893 pass 894 if timestamp is not None: 895 self.setTimestamp(timestamp) 896 897 def isError(self): 898 return self.getAttr('type') == 'error' 899 900 def isResult(self): 901 return self.getAttr('type') == 'result' 902 903 def getTo(self): 904 """ 905 Return value of the 'to' attribute 906 """ 907 try: 908 return self['to'] 909 except Exception: 910 pass 911 return None 912 913 def getFrom(self): 914 """ 915 Return value of the 'from' attribute 916 """ 917 try: 918 return self['from'] 919 except Exception: 920 pass 921 return None 922 923 def getTimestamp(self): 924 """ 925 Return the timestamp in the 'yyyymmddThhmmss' format 926 """ 927 if self.timestamp: 928 return self.timestamp 929 return time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) 930 931 def getTimestamp2(self): 932 """ 933 Return the timestamp in the 'yyyymmddThhmmss' format 934 """ 935 if self.timestamp: 936 return self.timestamp 937 return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) 938 939 def getJid(self): 940 """ 941 Return the value of the 'jid' attribute 942 """ 943 attr = self.getAttr('jid') 944 if attr: 945 return JID.from_string(attr) 946 return attr 947 948 def getID(self): 949 """ 950 Return the value of the 'id' attribute 951 """ 952 return self.getAttr('id') 953 954 def setTo(self, val): 955 """ 956 Set the value of the 'to' attribute 957 """ 958 if not isinstance(val, JID): 959 val = JID.from_string(val) 960 self.setAttr('to', val) 961 962 def getType(self): 963 """ 964 Return the value of the 'type' attribute 965 """ 966 return self.getAttr('type') 967 968 def setFrom(self, val): 969 """ 970 Set the value of the 'from' attribute 971 """ 972 if not isinstance(val, JID): 973 val = JID.from_string(val) 974 self.setAttr('from', val) 975 976 def setType(self, val): 977 """ 978 Set the value of the 'type' attribute 979 """ 980 self.setAttr('type', val) 981 982 def setID(self, val): 983 """ 984 Set the value of the 'id' attribute 985 """ 986 self.setAttr('id', val) 987 988 def getError(self): 989 """ 990 Return the error-condition (if present) or the textual description 991 of the error (otherwise) 992 """ 993 errtag = self.getTag('error') 994 if errtag is None: 995 return None 996 for tag in errtag.getChildren(): 997 if (tag.getName() != 'text' and 998 tag.getNamespace() == Namespace.STANZAS): 999 return tag.getName() 1000 return None 1001 1002 def getAppError(self): 1003 errtag = self.getTag('error') 1004 if errtag is None: 1005 return None 1006 for tag in errtag.getChildren(): 1007 if (tag.getName() != 'text' and 1008 tag.getNamespace() != Namespace.STANZAS): 1009 return tag.getName() 1010 return None 1011 1012 def getAppErrorNamespace(self): 1013 errtag = self.getTag('error') 1014 if errtag is None: 1015 return None 1016 for tag in errtag.getChildren(): 1017 if (tag.getName() != 'text' and 1018 tag.getNamespace() != Namespace.STANZAS): 1019 return tag.getNamespace() 1020 return None 1021 1022 def getErrorMsg(self): 1023 """ 1024 Return the textual description of the error (if present) 1025 or the error condition 1026 """ 1027 errtag = self.getTag('error') 1028 if errtag: 1029 for tag in errtag.getChildren(): 1030 if tag.getName() == 'text': 1031 return tag.getData() 1032 return self.getError() 1033 return None 1034 1035 def getErrorCode(self): 1036 """ 1037 Return the error code. Obsolete. 1038 """ 1039 return self.getTagAttr('error', 'code') 1040 1041 def getErrorType(self): 1042 """ 1043 Return the error code. Obsolete. 1044 """ 1045 return self.getTagAttr('error', 'type') 1046 1047 def getStatusConditions(self, as_code=False): 1048 """ 1049 Return the status conditions list as defined in XEP-0306. 1050 """ 1051 result = set() 1052 status_tags = self.getTags('status') 1053 for status in status_tags: 1054 if as_code: 1055 code = status.getAttr('code') 1056 if code is not None: 1057 result.add(code) 1058 else: 1059 for condition in status.getChildren(): 1060 result.add(condition.getName()) 1061 return list(result) 1062 1063 def setError(self, error, code=None): 1064 """ 1065 Set the error code. Obsolete. Use error-conditions instead 1066 """ 1067 if code: 1068 if str(code) in _errorcodes.keys(): 1069 error = ErrorNode(_errorcodes[str(code)], text=error) 1070 else: 1071 error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code, 1072 typ='cancel', text=error) 1073 elif isinstance(error, str): 1074 error = ErrorNode(error) 1075 self.setType('error') 1076 self.addChild(node=error) 1077 1078 def setTimestamp(self, val=None): 1079 """ 1080 Set the timestamp. timestamp should be the yyyymmddThhmmss string 1081 """ 1082 if not val: 1083 val = time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) 1084 self.timestamp=val 1085 self.setTag('x', {'stamp': self.timestamp}, namespace=Namespace.DELAY) 1086 1087 def getProperties(self): 1088 """ 1089 Return the list of namespaces to which belongs the 1090 direct childs of element 1091 """ 1092 props = [] 1093 for child in self.getChildren(): 1094 prop = child.getNamespace() 1095 if prop not in props: 1096 props.append(prop) 1097 return props 1098 1099 def getTag(self, name, attrs=None, namespace=None, protocol=False): 1100 """ 1101 Return the Node instance for the tag. 1102 If protocol is True convert to a new Protocol/Message instance. 1103 """ 1104 tag = Node.getTag(self, name, attrs, namespace) 1105 if protocol and tag: 1106 if name == 'message': 1107 return Message(node=tag) 1108 return Protocol(node=tag) 1109 return tag 1110 1111 def __setitem__(self, item, val): 1112 """ 1113 Set the item 'item' to the value 'val' 1114 """ 1115 if item in ['to', 'from']: 1116 if not isinstance(val, JID): 1117 val = JID.from_string(val) 1118 return self.setAttr(item, val) 1119 1120 1121class Message(Protocol): 1122 """ 1123 XMPP Message stanza - "push" mechanism 1124 """ 1125 1126 def __init__(self, 1127 to=None, 1128 body=None, 1129 xhtml=None, 1130 typ=None, 1131 subject=None, 1132 attrs=None, 1133 frm=None, 1134 payload=None, 1135 timestamp=None, 1136 xmlns=Namespace.CLIENT, 1137 node=None): 1138 """ 1139 You can specify recipient, text of message, type of message any 1140 additional attributes, sender of the message, any additional payload 1141 (f.e. jabber:x:delay element) and namespace in one go. 1142 1143 Alternatively you can pass in the other XML object as the 'node' 1144 parameted to replicate it as message 1145 """ 1146 Protocol.__init__(self, 1147 'message', 1148 to=to, 1149 typ=typ, 1150 attrs=attrs, 1151 frm=frm, 1152 payload=payload, 1153 timestamp=timestamp, 1154 xmlns=xmlns, 1155 node=node) 1156 if body: 1157 self.setBody(body) 1158 if xhtml is not None: 1159 self.setXHTML(xhtml) 1160 if subject is not None: 1161 self.setSubject(subject) 1162 1163 def getBody(self): 1164 """ 1165 Return text of the message 1166 """ 1167 return self.getTagData('body') 1168 1169 def getXHTML(self): 1170 return self.getTag('html', namespace=Namespace.XHTML_IM) 1171 1172 def getSubject(self): 1173 """ 1174 Return subject of the message 1175 """ 1176 return self.getTagData('subject') 1177 1178 def getThread(self): 1179 """ 1180 Return thread of the message 1181 """ 1182 return self.getTagData('thread') 1183 1184 def getOriginID(self): 1185 """ 1186 Return origin-id of the message 1187 """ 1188 return self.getTagAttr('origin-id', namespace=Namespace.SID, attr='id') 1189 1190 def getStanzaIDAttrs(self): 1191 """ 1192 Return the stanza-id attributes of the message 1193 """ 1194 try: 1195 attrs = self.getTag('stanza-id', namespace=Namespace.SID).getAttrs() 1196 except Exception: 1197 return None, None 1198 return attrs['id'], attrs['by'] 1199 1200 def setBody(self, val): 1201 """ 1202 Set the text of the message""" 1203 self.setTagData('body', val) 1204 1205 def setXHTML(self, body, add=False): 1206 if isinstance(body, str): 1207 body = Node(node=body) 1208 if add: 1209 xhtml = self.getTag('html', namespace=Namespace.XHTML_IM) 1210 if xhtml is not None: 1211 xhtml.addChild(node=body) 1212 else: 1213 self.addChild('html', 1214 namespace=Namespace.XHTML_IM, 1215 payload=body) 1216 else: 1217 xhtml_nodes = self.getTags('html', namespace=Namespace.XHTML_IM) 1218 for xhtml in xhtml_nodes: 1219 self.delChild(xhtml) 1220 self.addChild('html', namespace=Namespace.XHTML_IM, payload=body) 1221 1222 def setSubject(self, val): 1223 """ 1224 Set the subject of the message 1225 """ 1226 self.setTagData('subject', val) 1227 1228 def setThread(self, val): 1229 """ 1230 Set the thread of the message 1231 """ 1232 self.setTagData('thread', val) 1233 1234 def setOriginID(self, val): 1235 """ 1236 Sets the origin-id of the message 1237 """ 1238 self.setTag('origin-id', namespace=Namespace.SID, attrs={'id': val}) 1239 1240 def buildReply(self, text=None): 1241 """ 1242 Builds and returns another message object with specified text. The to, 1243 from, thread and type properties of new message are pre-set as reply to 1244 this message 1245 """ 1246 m = Message(to=self.getFrom(), 1247 frm=self.getTo(), 1248 body=text, 1249 typ=self.getType()) 1250 th = self.getThread() 1251 if th: 1252 m.setThread(th) 1253 return m 1254 1255 def getStatusCode(self): 1256 """ 1257 Return the status code of the message (for groupchat config change) 1258 """ 1259 attrs = [] 1260 for xtag in self.getTags('x'): 1261 for child in xtag.getTags('status'): 1262 attrs.append(child.getAttr('code')) 1263 return attrs 1264 1265 def setMarker(self, type_, id_): 1266 self.setTag(type_, namespace=Namespace.CHATMARKERS, attrs={'id': id_}) 1267 1268 def setMarkable(self): 1269 self.setTag('markable', namespace=Namespace.CHATMARKERS) 1270 1271 def setReceiptRequest(self): 1272 self.setTag('request', namespace=Namespace.RECEIPTS) 1273 1274 def setReceiptReceived(self, id_): 1275 self.setTag('received', namespace=Namespace.RECEIPTS, attrs={'id': id_}) 1276 1277 def setOOB(self, url, desc=None): 1278 oob = self.setTag('x', namespace=Namespace.X_OOB) 1279 oob.setTagData('url', url) 1280 if desc is not None: 1281 oob.setTagData('desc', desc) 1282 1283 def setCorrection(self, id_): 1284 self.setTag('replace', namespace=Namespace.CORRECT, attrs={'id': id_}) 1285 1286 def setAttention(self): 1287 self.setTag('attention', namespace=Namespace.ATTENTION) 1288 1289 def setHint(self, hint): 1290 self.setTag(hint, namespace=Namespace.HINTS) 1291 1292 1293class Presence(Protocol): 1294 1295 def __init__(self, 1296 to=None, 1297 typ=None, 1298 priority=None, 1299 show=None, 1300 status=None, 1301 attrs=None, 1302 frm=None, 1303 timestamp=None, 1304 payload=None, 1305 xmlns=Namespace.CLIENT, 1306 node=None): 1307 """ 1308 You can specify recipient, type of message, priority, show and status 1309 values any additional attributes, sender of the presence, timestamp, any 1310 additional payload (f.e. jabber:x:delay element) and namespace in one 1311 go. Alternatively you can pass in the other XML object as the 'node' 1312 parameted to replicate it as presence 1313 """ 1314 Protocol.__init__(self, 1315 'presence', 1316 to=to, 1317 typ=typ, 1318 attrs=attrs, 1319 frm=frm, 1320 payload=payload, 1321 timestamp=timestamp, 1322 xmlns=xmlns, 1323 node=node) 1324 if priority: 1325 self.setPriority(priority) 1326 if show: 1327 self.setShow(show) 1328 if status: 1329 self.setStatus(status) 1330 1331 def getPriority(self): 1332 """ 1333 Return the priority of the message 1334 """ 1335 return self.getTagData('priority') 1336 1337 def getShow(self): 1338 """ 1339 Return the show value of the message 1340 """ 1341 return self.getTagData('show') 1342 1343 def getStatus(self): 1344 """ 1345 Return the status string of the message 1346 """ 1347 return self.getTagData('status') or '' 1348 1349 def setPriority(self, val): 1350 """ 1351 Set the priority of the message 1352 """ 1353 self.setTagData('priority', val) 1354 1355 def setShow(self, val): 1356 """ 1357 Set the show value of the message 1358 """ 1359 if val not in ['away', 'chat', 'dnd', 'xa']: 1360 raise ValueError('Invalid show value: %s' % val) 1361 self.setTagData('show', val) 1362 1363 def setStatus(self, val): 1364 """ 1365 Set the status string of the message 1366 """ 1367 self.setTagData('status', val) 1368 1369 def _muc_getItemAttr(self, tag, attr): 1370 for xtag in self.getTags('x'): 1371 if xtag.getNamespace() not in (Namespace.MUC_USER, 1372 Namespace.MUC_ADMIN): 1373 continue 1374 for child in xtag.getTags(tag): 1375 return child.getAttr(attr) 1376 1377 def _muc_getSubTagDataAttr(self, tag, attr): 1378 for xtag in self.getTags('x'): 1379 if xtag.getNamespace() not in (Namespace.MUC_USER, 1380 Namespace.MUC_ADMIN): 1381 continue 1382 for child in xtag.getTags('item'): 1383 for cchild in child.getTags(tag): 1384 return cchild.getData(), cchild.getAttr(attr) 1385 return None, None 1386 1387 def getRole(self): 1388 """ 1389 Return the presence role (for groupchat) 1390 """ 1391 return self._muc_getItemAttr('item', 'role') 1392 1393 def getAffiliation(self): 1394 """ 1395 Return the presence affiliation (for groupchat) 1396 """ 1397 return self._muc_getItemAttr('item', 'affiliation') 1398 1399 def getNewNick(self): 1400 """ 1401 Return the status code of the presence (for groupchat) 1402 """ 1403 return self._muc_getItemAttr('item', 'nick') 1404 1405 def getJid(self): 1406 """ 1407 Return the presence jid (for groupchat) 1408 """ 1409 return self._muc_getItemAttr('item', 'jid') 1410 1411 def getReason(self): 1412 """ 1413 Returns the reason of the presence (for groupchat) 1414 """ 1415 return self._muc_getSubTagDataAttr('reason', '')[0] 1416 1417 def getActor(self): 1418 """ 1419 Return the reason of the presence (for groupchat) 1420 """ 1421 return self._muc_getSubTagDataAttr('actor', 'jid')[1] 1422 1423 def getStatusCode(self): 1424 """ 1425 Return the status code of the presence (for groupchat) 1426 """ 1427 attrs = [] 1428 for xtag in self.getTags('x'): 1429 for child in xtag.getTags('status'): 1430 attrs.append(child.getAttr('code')) 1431 return attrs 1432 1433class Iq(Protocol): 1434 """ 1435 XMPP Iq object - get/set dialog mechanism 1436 """ 1437 1438 def __init__(self, 1439 typ=None, 1440 queryNS=None, 1441 attrs=None, 1442 to=None, 1443 frm=None, 1444 payload=None, 1445 xmlns=Namespace.CLIENT, 1446 node=None): 1447 """ 1448 You can specify type, query namespace any additional attributes, 1449 recipient of the iq, sender of the iq, any additional payload (f.e. 1450 jabber:x:data node) and namespace in one go. 1451 1452 Alternatively you can pass in the other XML object as the 'node' 1453 parameted to replicate it as an iq 1454 """ 1455 Protocol.__init__(self, 1456 'iq', 1457 to=to, 1458 typ=typ, 1459 attrs=attrs, 1460 frm=frm, 1461 xmlns=xmlns, 1462 node=node) 1463 if payload: 1464 self.setQueryPayload(payload) 1465 if queryNS: 1466 self.setQueryNS(queryNS) 1467 1468 def getQuery(self): 1469 """ 1470 Return the IQ's child element if it exists, None otherwise. 1471 """ 1472 children = self.getChildren() 1473 if children and self.getType() != 'error' and \ 1474 children[0].getName() != 'error': 1475 return children[0] 1476 return None 1477 1478 def getQueryNS(self): 1479 """ 1480 Return the namespace of the 'query' child element 1481 """ 1482 tag = self.getQuery() 1483 if tag: 1484 return tag.getNamespace() 1485 return None 1486 1487 def getQuerynode(self): 1488 """ 1489 Return the 'node' attribute value of the 'query' child element 1490 """ 1491 tag = self.getQuery() 1492 if tag: 1493 return tag.getAttr('node') 1494 return None 1495 1496 def getQueryPayload(self): 1497 """ 1498 Return the 'query' child element payload 1499 """ 1500 tag = self.getQuery() 1501 if tag: 1502 return tag.getPayload() 1503 return None 1504 1505 def getQueryChildren(self): 1506 """ 1507 Return the 'query' child element child nodes 1508 """ 1509 tag = self.getQuery() 1510 if tag: 1511 return tag.getChildren() 1512 return None 1513 1514 def getQueryChild(self, name=None): 1515 """ 1516 Return the 'query' child element with name, or the first element 1517 which is not an error element 1518 """ 1519 query = self.getQuery() 1520 if not query: 1521 return None 1522 for node in query.getChildren(): 1523 if name is not None: 1524 if node.getName() == name: 1525 return node 1526 else: 1527 if node.getName() != 'error': 1528 return node 1529 return None 1530 1531 def setQuery(self, name=None): 1532 """ 1533 Change the name of the query node, creating it if needed. Keep the 1534 existing name if none is given (use 'query' if it's a creation). 1535 Return the query node. 1536 """ 1537 query = self.getQuery() 1538 if query is None: 1539 query = self.addChild('query') 1540 if name is not None: 1541 query.setName(name) 1542 return query 1543 1544 def setQueryNS(self, namespace): 1545 """ 1546 Set the namespace of the 'query' child element 1547 """ 1548 self.setQuery().setNamespace(namespace) 1549 1550 def setQueryPayload(self, payload): 1551 """ 1552 Set the 'query' child element payload 1553 """ 1554 self.setQuery().setPayload(payload) 1555 1556 def setQuerynode(self, node): 1557 """ 1558 Set the 'node' attribute value of the 'query' child element 1559 """ 1560 self.setQuery().setAttr('node', node) 1561 1562 def buildReply(self, typ): 1563 """ 1564 Build and return another Iq object of specified type. The to, from and 1565 query child node of new Iq are pre-set as reply to this Iq. 1566 """ 1567 iq = Iq(typ, 1568 to=self.getFrom(), 1569 frm=self.getTo(), 1570 attrs={'id': self.getID()}) 1571 iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS()) 1572 return iq 1573 1574 def buildSimpleReply(self, typ): 1575 return Iq(typ, 1576 to=self.getFrom(), 1577 attrs={'id': self.getID()}) 1578 1579 1580class Hashes(Node): 1581 """ 1582 Hash elements for various XEPs as defined in XEP-300 1583 1584 RECOMENDED HASH USE: 1585 Algorithm Support 1586 MD2 MUST NOT 1587 MD4 MUST NOT 1588 MD5 MAY 1589 SHA-1 MUST 1590 SHA-256 MUST 1591 SHA-512 SHOULD 1592 """ 1593 1594 supported = ('md5', 'sha-1', 'sha-256', 'sha-512') 1595 1596 def __init__(self, nsp=Namespace.HASHES): 1597 Node.__init__(self, None, {}, [], None, None, False, None) 1598 self.setNamespace(nsp) 1599 self.setName('hash') 1600 1601 def calculateHash(self, algo, file_string): 1602 """ 1603 Calculate the hash and add it. It is preferable doing it here 1604 instead of doing it all over the place in Gajim. 1605 """ 1606 hl = None 1607 hash_ = None 1608 # file_string can be a string or a file 1609 if isinstance(file_string, str): 1610 if algo == 'sha-1': 1611 hl = hashlib.sha1() 1612 elif algo == 'md5': 1613 hl = hashlib.md5() 1614 elif algo == 'sha-256': 1615 hl = hashlib.sha256() 1616 elif algo == 'sha-512': 1617 hl = hashlib.sha512() 1618 if hl: 1619 hl.update(file_string) 1620 hash_ = hl.hexdigest() 1621 else: # if it is a file 1622 if algo == 'sha-1': 1623 hl = hashlib.sha1() 1624 elif algo == 'md5': 1625 hl = hashlib.md5() 1626 elif algo == 'sha-256': 1627 hl = hashlib.sha256() 1628 elif algo == 'sha-512': 1629 hl = hashlib.sha512() 1630 if hl: 1631 for line in file_string: 1632 hl.update(line) 1633 hash_ = hl.hexdigest() 1634 return hash_ 1635 1636 def addHash(self, hash_, algo): 1637 self.setAttr('algo', algo) 1638 self.setData(hash_) 1639 1640class Hashes2(Node): 1641 """ 1642 Hash elements for various XEPs as defined in XEP-300 1643 1644 RECOMENDED HASH USE: 1645 Algorithm Support 1646 MD2 MUST NOT 1647 MD4 MUST NOT 1648 MD5 MUST NOT 1649 SHA-1 SHOULD NOT 1650 SHA-256 MUST 1651 SHA-512 SHOULD 1652 SHA3-256 MUST 1653 SHA3-512 SHOULD 1654 BLAKE2b256 MUST 1655 BLAKE2b512 SHOULD 1656 """ 1657 1658 supported = ('sha-256', 'sha-512', 'sha3-256', 1659 'sha3-512', 'blake2b-256', 'blake2b-512') 1660 1661 def __init__(self, nsp=Namespace.HASHES_2): 1662 Node.__init__(self, None, {}, [], None, None, False, None) 1663 self.setNamespace(nsp) 1664 self.setName('hash') 1665 1666 def calculateHash(self, algo, file_string): 1667 """ 1668 Calculate the hash and add it. It is preferable doing it here 1669 instead of doing it all over the place in Gajim. 1670 """ 1671 hl = None 1672 hash_ = None 1673 if algo == 'sha-256': 1674 hl = hashlib.sha256() 1675 elif algo == 'sha-512': 1676 hl = hashlib.sha512() 1677 elif algo == 'sha3-256': 1678 hl = hashlib.sha3_256() 1679 elif algo == 'sha3-512': 1680 hl = hashlib.sha3_512() 1681 elif algo == 'blake2b-256': 1682 hl = hashlib.blake2b(digest_size=32) 1683 elif algo == 'blake2b-512': 1684 hl = hashlib.blake2b(digest_size=64) 1685 # file_string can be a string or a file 1686 if hl is not None: 1687 if isinstance(file_string, bytes): 1688 hl.update(file_string) 1689 else: # if it is a file 1690 for line in file_string: 1691 hl.update(line) 1692 hash_ = b64encode(hl.digest()).decode('ascii') 1693 return hash_ 1694 1695 def addHash(self, hash_, algo): 1696 self.setAttr('algo', algo) 1697 self.setData(hash_) 1698 1699 1700class BindRequest(Iq): 1701 def __init__(self, resource): 1702 if resource is not None: 1703 resource = Node('resource', payload=resource) 1704 Iq.__init__(self, typ='set') 1705 self.addChild(node=Node('bind', 1706 {'xmlns': Namespace.BIND}, 1707 payload=resource)) 1708 1709 1710class TLSRequest(Node): 1711 def __init__(self): 1712 Node.__init__(self, tag='starttls', attrs={'xmlns': Namespace.TLS}) 1713 1714 1715class SessionRequest(Iq): 1716 def __init__(self): 1717 Iq.__init__(self, typ='set') 1718 self.addChild(node=Node('session', attrs={'xmlns': Namespace.SESSION})) 1719 1720 1721class StreamHeader(Node): 1722 def __init__(self, domain, lang=None): 1723 if lang is None: 1724 lang = 'en' 1725 Node.__init__(self, 1726 tag='stream:stream', 1727 attrs={'xmlns': Namespace.CLIENT, 1728 'version': '1.0', 1729 'xmlns:stream': Namespace.STREAMS, 1730 'to': domain, 1731 'xml:lang': lang}) 1732 1733 1734class WebsocketOpenHeader(Node): 1735 def __init__(self, domain, lang=None): 1736 if lang is None: 1737 lang = 'en' 1738 Node.__init__(self, 1739 tag='open', 1740 attrs={'xmlns': Namespace.FRAMING, 1741 'version': '1.0', 1742 'to': domain, 1743 'xml:lang': lang}) 1744 1745class WebsocketCloseHeader(Node): 1746 def __init__(self): 1747 Node.__init__(self, tag='close', attrs={'xmlns': Namespace.FRAMING}) 1748 1749 1750class Features(Node): 1751 def __init__(self, node): 1752 Node.__init__(self, node=node) 1753 1754 def has_starttls(self): 1755 tls = self.getTag('starttls', namespace=Namespace.TLS) 1756 if tls is not None: 1757 required = tls.getTag('required') is not None 1758 return True, required 1759 return False, False 1760 1761 def has_sasl(self): 1762 return self.getTag('mechanisms', namespace=Namespace.SASL) is not None 1763 1764 def get_mechs(self): 1765 mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL) 1766 if mechanisms is None: 1767 return set() 1768 mechanisms = mechanisms.getTags('mechanism') 1769 return set(mech.getData() for mech in mechanisms) 1770 1771 def get_domain_based_name(self): 1772 hostname = self.getTag('hostname', 1773 namespace=Namespace.DOMAIN_BASED_NAME) 1774 if hostname is not None: 1775 return hostname.getData() 1776 return None 1777 1778 def has_bind(self): 1779 return self.getTag('bind', namespace=Namespace.BIND) is not None 1780 1781 def session_required(self): 1782 session = self.getTag('session', namespace=Namespace.SESSION) 1783 if session is not None: 1784 optional = session.getTag('optional') is not None 1785 return not optional 1786 return False 1787 1788 def has_sm(self): 1789 return self.getTag('sm', namespace=Namespace.STREAM_MGMT) is not None 1790 1791 def has_roster_version(self): 1792 return self.getTag('ver', namespace=Namespace.ROSTER_VER) is not None 1793 1794 def has_register(self): 1795 return self.getTag( 1796 'register', namespace=Namespace.REGISTER_FEATURE) is not None 1797 1798 def has_anonymous(self): 1799 return 'ANONYMOUS' in self.get_mechs() 1800 1801 1802class ErrorNode(Node): 1803 """ 1804 XMPP-style error element 1805 1806 In the case of stanza error should be attached to XMPP stanza. 1807 In the case of stream-level errors should be used separately. 1808 """ 1809 1810 def __init__(self, name, code=None, typ=None, text=None): 1811 """ 1812 Mandatory parameter: name - name of error condition. 1813 Optional parameters: code, typ, text. 1814 Used for backwards compartibility with older jabber protocol. 1815 """ 1816 if name in ERRORS: 1817 cod, type_, txt = ERRORS[name] 1818 ns = name.split()[0] 1819 else: 1820 cod, ns, type_, txt = '500', Namespace.STANZAS, 'cancel', '' 1821 if typ: 1822 type_ = typ 1823 if code: 1824 cod = code 1825 if text: 1826 txt = text 1827 Node.__init__(self, 'error', {}, [Node(name)]) 1828 if type_: 1829 self.setAttr('type', type_) 1830 if not cod: 1831 self.setName('stream:error') 1832 if txt: 1833 self.addChild(node=Node(ns + ' text', {}, [txt])) 1834 if cod: 1835 self.setAttr('code', cod) 1836 1837class Error(Protocol): 1838 """ 1839 Used to quickly transform received stanza into error reply 1840 """ 1841 1842 def __init__(self, node, error, reply=1): 1843 """ 1844 Create error reply basing on the received 'node' stanza and the 'error' 1845 error condition 1846 1847 If the 'node' is not the received stanza but locally created ('to' and 1848 'from' fields needs not swapping) specify the 'reply' argument as false. 1849 """ 1850 if reply: 1851 Protocol.__init__(self, 1852 to=node.getFrom(), 1853 frm=node.getTo(), 1854 node=node) 1855 else: 1856 Protocol.__init__(self, node=node) 1857 self.setError(error) 1858 if node.getType() == 'error': 1859 self.__str__ = self.__dupstr__ 1860 1861 def __dupstr__(self, _dup1=None, _dup2=None): 1862 """ 1863 Dummy function used as preventor of creating error node in reply to 1864 error node. I.e. you will not be able to serialise "double" error 1865 into string. 1866 """ 1867 return '' 1868 1869class DataField(Node): 1870 """ 1871 This class is used in the DataForm class to describe the single data item 1872 1873 If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) then 1874 you will need to work with instances of this class. 1875 """ 1876 1877 def __init__(self, 1878 name=None, 1879 value=None, 1880 typ=None, 1881 required=0, 1882 desc=None, 1883 options=None, 1884 node=None): 1885 """ 1886 Create new data field of specified name,value and type 1887 1888 Also 'required','desc' and 'options' fields can be set. Alternatively 1889 other XML object can be passed in as the 'node' parameted 1890 to replicate it as a new datafiled. 1891 """ 1892 Node.__init__(self, 'field', node=node) 1893 if name: 1894 self.setVar(name) 1895 if isinstance(value, (list, tuple)): 1896 self.setValues(value) 1897 elif value: 1898 self.setValue(value) 1899 if typ: 1900 self.setType(typ) 1901 elif not typ and not node: 1902 self.setType('text-single') 1903 if required: 1904 self.setRequired(required) 1905 if desc: 1906 self.setDesc(desc) 1907 if options: 1908 self.setOptions(options) 1909 1910 def setRequired(self, req=1): 1911 """ 1912 Change the state of the 'required' flag 1913 """ 1914 if req: 1915 self.setTag('required') 1916 else: 1917 try: 1918 self.delChild('required') 1919 except ValueError: 1920 return 1921 1922 def isRequired(self): 1923 """ 1924 Return in this field a required one 1925 """ 1926 return self.getTag('required') 1927 1928 def setDesc(self, desc): 1929 """ 1930 Set the description of this field 1931 """ 1932 self.setTagData('desc', desc) 1933 1934 def getDesc(self): 1935 """ 1936 Return the description of this field 1937 """ 1938 return self.getTagData('desc') 1939 1940 def setValue(self, val): 1941 """ 1942 Set the value of this field 1943 """ 1944 self.setTagData('value', val) 1945 1946 def getValue(self): 1947 return self.getTagData('value') 1948 1949 def setValues(self, lst): 1950 """ 1951 Set the values of this field as values-list. Replaces all previous filed 1952 values! If you need to just add a value - use addValue method 1953 """ 1954 while self.getTag('value'): 1955 self.delChild('value') 1956 for val in lst: 1957 self.addValue(val) 1958 1959 def addValue(self, val): 1960 """ 1961 Add one more value to this field. Used in 'get' iq's or such 1962 """ 1963 self.addChild('value', {}, [val]) 1964 1965 def getValues(self): 1966 """ 1967 Return the list of values associated with this field 1968 """ 1969 ret = [] 1970 for tag in self.getTags('value'): 1971 ret.append(tag.getData()) 1972 return ret 1973 1974 def getOptions(self): 1975 """ 1976 Return label-option pairs list associated with this field 1977 """ 1978 ret = [] 1979 for tag in self.getTags('option'): 1980 ret.append([tag.getAttr('label'), tag.getTagData('value')]) 1981 return ret 1982 1983 def setOptions(self, lst): 1984 """ 1985 Set label-option pairs list associated with this field 1986 """ 1987 while self.getTag('option'): 1988 self.delChild('option') 1989 for opt in lst: 1990 self.addOption(opt) 1991 1992 def addOption(self, opt): 1993 """ 1994 Add one more label-option pair to this field 1995 """ 1996 if isinstance(opt, list): 1997 self.addChild('option', 1998 {'label': opt[0]}).setTagData('value', opt[1]) 1999 else: 2000 self.addChild('option').setTagData('value', opt) 2001 2002 def getType(self): 2003 """ 2004 Get type of this field 2005 """ 2006 return self.getAttr('type') 2007 2008 def setType(self, val): 2009 """ 2010 Set type of this field 2011 """ 2012 return self.setAttr('type', val) 2013 2014 def getVar(self): 2015 """ 2016 Get 'var' attribute value of this field 2017 """ 2018 return self.getAttr('var') 2019 2020 def setVar(self, val): 2021 """ 2022 Set 'var' attribute value of this field 2023 """ 2024 return self.setAttr('var', val) 2025 2026class DataForm(Node): 2027 """ 2028 Used for manipulating dataforms in XMPP 2029 2030 Relevant XEPs: 0004, 0068, 0122. Can be used in disco, pub-sub and many 2031 other applications. 2032 """ 2033 def __init__(self, typ=None, data=None, title=None, node=None): 2034 """ 2035 Create new dataform of type 'typ'. 'data' is the list of DataField 2036 instances that this dataform contains, 'title' - the title string. You 2037 can specify the 'node' argument as the other node to be used as base for 2038 constructing this dataform 2039 2040 title and instructions is optional and SHOULD NOT contain newlines. 2041 Several instructions MAY be present. 2042 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' ) 2043 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) 2044 respectively. 2045 'cancel' form can not contain any fields. All other forms contains 2046 AT LEAST one field. 2047 'title' MAY be included in forms of type "form" and "result" 2048 """ 2049 Node.__init__(self, 'x', node=node) 2050 if node: 2051 newkids = [] 2052 for n in self.getChildren(): 2053 if n.getName() == 'field': 2054 newkids.append(DataField(node=n)) 2055 else: 2056 newkids.append(n) 2057 self.kids = newkids 2058 if typ: 2059 self.setType(typ) 2060 self.setNamespace(Namespace.DATA) 2061 if title: 2062 self.setTitle(title) 2063 if data is not None: 2064 if isinstance(data, dict): 2065 newdata = [] 2066 for name in data.keys(): 2067 newdata.append(DataField(name, data[name])) 2068 data = newdata 2069 for child in data: 2070 if child.__class__.__name__ == 'DataField': 2071 self.kids.append(child) 2072 elif isinstance(child, Node): 2073 self.kids.append(DataField(node=child)) 2074 else: # Must be a string 2075 self.addInstructions(child) 2076 2077 def getType(self): 2078 """ 2079 Return the type of dataform 2080 """ 2081 return self.getAttr('type') 2082 2083 def setType(self, typ): 2084 """ 2085 Set the type of dataform 2086 """ 2087 self.setAttr('type', typ) 2088 2089 def getTitle(self): 2090 """ 2091 Return the title of dataform 2092 """ 2093 return self.getTagData('title') 2094 2095 def setTitle(self, text): 2096 """ 2097 Set the title of dataform 2098 """ 2099 self.setTagData('title', text) 2100 2101 def getInstructions(self): 2102 """ 2103 Return the instructions of dataform 2104 """ 2105 return self.getTagData('instructions') 2106 2107 def setInstructions(self, text): 2108 """ 2109 Set the instructions of dataform 2110 """ 2111 self.setTagData('instructions', text) 2112 2113 def addInstructions(self, text): 2114 """ 2115 Add one more instruction to the dataform 2116 """ 2117 self.addChild('instructions', {}, [text]) 2118 2119 def getField(self, name): 2120 """ 2121 Return the datafield object with name 'name' (if exists) 2122 """ 2123 return self.getTag('field', attrs={'var': name}) 2124 2125 def setField(self, name): 2126 """ 2127 Create if nessessary or get the existing datafield object with name 2128 'name' and return it 2129 """ 2130 f = self.getField(name) 2131 if f: 2132 return f 2133 return self.addChild(node=DataField(name)) 2134 2135 def asDict(self): 2136 """ 2137 Represent dataform as simple dictionary mapping of datafield names to 2138 their values 2139 """ 2140 ret = {} 2141 for field in self.getTags('field'): 2142 name = field.getAttr('var') 2143 typ = field.getType() 2144 if typ and typ.endswith('-multi'): 2145 val = [] 2146 for i in field.getTags('value'): 2147 val.append(i.getData()) 2148 else: 2149 val = field.getTagData('value') 2150 ret[name] = val 2151 if self.getTag('instructions'): 2152 ret['instructions'] = self.getInstructions() 2153 return ret 2154 2155 def __getitem__(self, name): 2156 """ 2157 Simple dictionary interface for getting datafields values by their names 2158 """ 2159 item = self.getField(name) 2160 if item: 2161 return item.getValue() 2162 raise IndexError('No such field') 2163 2164 def __setitem__(self, name, val): 2165 """ 2166 Simple dictionary interface for setting datafields values by their names 2167 """ 2168 return self.setField(name).setValue(val) 2169