pax_global_header 0000666 0000000 0000000 00000000064 14130341156 0014510 g ustar 00root root 0000000 0000000 52 comment=159822db28977639a5b33a330186327288db1cce
python-nbxmpp-nbxmpp-2.0.4/ 0000775 0000000 0000000 00000000000 14130341156 0015640 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/.gitignore 0000664 0000000 0000000 00000000077 14130341156 0017634 0 ustar 00root root 0000000 0000000 build/
__pycache__/
.mypy_cache/
nbxmpp.egg-info
dist
.idea
*~
python-nbxmpp-nbxmpp-2.0.4/.gitlab-ci.yml 0000664 0000000 0000000 00000002566 14130341156 0020305 0 ustar 00root root 0000000 0000000 before_script:
- sudo apt-get update -qq && sudo apt-get build-dep -y -qq python3-nbxmpp-nightly
stages:
- test
- build
run-test:
stage: test
script:
- rm -rf civenv-nbxmpp
- virtualenv -p python3 --system-site-packages civenv-nbxmpp
- . ./civenv-nbxmpp/bin/activate
- pip3 install -I pylint==2.4.4
- pip3 install -I coverage
- pip3 install -I idna
- pip3 install -I precis-i18n
- python3 -m pylint nbxmpp --disable=C0103,C0201,C0301,C0326,C0330,W0201,W0212,W0221,W0231,W0233,W0621,W0622,R0201,E1101,E1135
- coverage run --source=nbxmpp -m unittest discover -v
- coverage report -mi
- coverage xml -i
- deactivate
- rm -rf civenv-nbxmpp
coverage: "/TOTAL.+ ([0-9]{1,3}%)/"
artifacts:
reports:
cobertura: coverage.xml
# C0103 invalid-name
# C0201 consider-iterating-dictionary
# C0301 line-too-long
# C0326 bad-whitespace
# C0330 bad-continuation
# W0201 attribute-defined-outside-init
# W0212 protected-access
# W0221 arguments-differ
# W0231 super-init-not-called
# W0233 non-parent-init-called
# W0621 redefined-outer-name
# W0622 redefined-builtin
# R0201 no-self-use
# E1101 no-member
# E1135 unsupported-membership-test
run-build:
stage: build
script:
- rm -rf dist
- python3 setup.py sdist
artifacts:
name: "nbxmpp-$CI_COMMIT_SHA"
expire_in: 1 week
paths:
- dist/nbxmpp-*.tar.gz
python-nbxmpp-nbxmpp-2.0.4/.pylintrc 0000664 0000000 0000000 00000031237 14130341156 0017513 0 ustar 00root root 0000000 0000000 [MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=pycurl
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint.
jobs=2
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
# C0111 missing-docstring
# W0511 fix-me
disable=C0111,W0511
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=(_.*|kwargs|args)
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,gi.repository.GLib.Error
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )??$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=80
# Maximum number of lines in a module
max-module-lines=3000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[BASIC]
# Naming hint for argument names
argument-name-hint=(([a-z][a-z0-9_]{1,30})|(_[a-z0-9_]*))$
# Regular expression matching correct argument names
argument-rgx=(([a-z][a-z0-9_]{1,30})|(_[a-z0-9_]*))$
# Naming hint for attribute names
attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct attribute names
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct constant names
const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming hint for function names
function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct function names
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_,iq
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for method names
method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct method names
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Naming hint for variable names
variable-name-hint=(([a-z][a-z0-9_]{1,30})|(_[a-z0-9_]*))$
# Regular expression matching correct variable names
variable-rgx=(([a-z][a-z0-9_]{1,30})|(_[a-z0-9_]*))$
[IMPORTS]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[DESIGN]
# Maximum number of arguments for function / method
max-args=15
# Maximum number of attributes for a class (see R0902).
max-attributes=9999
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
# Maximum number of branch for function / method body
max-branches=50
# Maximum number of locals for function / method body
max-locals=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=100
# Maximum number of return / yield for function / method body
max-returns=100
# Maximum number of statements in function / method body
max-statements=100
# Minimum number of public methods for a class (see R0903).
min-public-methods=0
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=
python-nbxmpp-nbxmpp-2.0.4/COPYING 0000664 0000000 0000000 00000104513 14130341156 0016677 0 ustar 00root root 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
python-nbxmpp-nbxmpp-2.0.4/ChangeLog 0000664 0000000 0000000 00000011203 14130341156 0017407 0 ustar 00root root 0000000 0000000 python-nbxmpp 2.0.4 (09 October 2021)
Bugfixes
* Ignore messages with incorrect id
* AdHoc: Make parsing AdHoc commands more compliant
python-nbxmpp 2.0.3 (24 July 2021)
Bugfixes
* Correctly handle RSM count request
* Improve XEP-0050 Compliance
* GSSAPI: Catch OSError
python-nbxmpp 2.0.2 (18 February 2021)
Bugfixes
* Add get_text() for Error Base class
* Client: Remove GSSAPI from default mechanisms
* Presence: Fix is_nickname_changed check
* UserAvatar: Add default argument
python-nbxmpp 2.0.1 (07 February 2021)
New
* Properties: is_from_us() Method
Bugfixes
* Fix handling invalid websocket uris
python-nbxmpp 2.0.0 (29 December 2020)
New
* New JID object
* Add JID Escaping (XEP-0106) support
* Add VCard4 (XEP-0292) support
* Make module calls generator based
* Add GSSAPI support (XEP-0233)
* Simplify and harmonize module API
python-nbxmpp 1.0.2 (14 August 2020)
New
* Client: Expose more information about the connection
* Client: set_ignored_tls_errors() allow to pass None
* Add method to disable stream management
Bugfixes
* TCP: Set input/output closed on finalize()
* TCP: Catch Runtime error
* Perform UTS46 mapping on domain names
python-nbxmpp 1.0.1 (07 July 2020)
New
* Add XEP-0377 support
* MUC: Return message id when using inivite()
Bugfixes
* OMEMO: Correctly parse prekey value
* Client: Determine protocol and type correctly if a custom host is used
* Smacks: Don't fail on saving error replies
python-nbxmpp 1.0.0 (18 June 2020)
* Library rewritten in most parts
* Replace BOSH with Websocket
* Add new example client
* Many other improvements
Known Issues:
* Currently no Client Cert support
python-nbxmpp 0.6.10 (19 February 2019)
* Add support for domain based name in features for GSSAPI
* Fix usage of BOSH
* Fix Jingle hash namespace
python-nbxmpp 0.6.9 (10 January 2019)
* Always bind after SM failed Fixes #64
* Dont try and guess system language
python-nbxmpp 0.6.8 (07 October 2018)
* Reset SM counter after receiving
* Issue event when SM resume fails
python-nbxmpp 0.6.7 (19 August 2018)
* Raise default TLS version to 1.2
* Remove DIGEST-MD5 from the default auth mechs
* Add STANZA RECEIVED Event (Some servers send more than one stanza in one packet)
* Add alternative locations to load TLS certs from
python-nbxmpp 0.6.6 (20 May 2018)
* Record all SSL errors instead of only the first
* Pass arguments to plugin/plugout methods
* Allow the Roster to be initalized without requesting it from the server
python-nbxmpp 0.6.5 (30 April 2018)
* Fix BOSH usage (don't set ALPN)
* Better handling of certificate files
python-nbxmpp 0.6.4 (17 March 2018)
* Fix SOCKS5 usage
python-nbxmpp 0.6.3 (26 January 2018)
* Add ALPN and SNI support for when using DirectTLS
* Bugfixes
python-nbxmpp 0.6.2 (27 December 2017)
* Correctly load client certs
* Warn on any error in the certificate chain
* Fixed a traceback loop
python-nbxmpp 0.6.1 (29 November 2017)
* Add new getStanzaIDAttrs method
* Fix BOSH connexion
* stop using PyOpenSSL for random generator
python-nbxmpp 0.6.0 (25 September 2017)
* Add new getOriginID/SetOriginID method for Messages
* Add new getJid() method for Protocol
* getTagAttr() accepts now a namespace argument
* Add new `protocol` argument for getTag()
* Add new XEP Namespaces
python-nbxmpp 0.5.6 (03 June 2017)
* Support XEP-0198 Version 1.5.2
* Add new XEP Namespaces
python-nbxmpp 0.5.5 (30 January 2017)
* Some cleanup
* Add some namespaces
python-nbxmpp 0.5.4 (04 September 2016)
* Fix SCRAM authentication
* Fix BOSH connection with UTF-8 messages
* Fix smacks implementation
* Use uuid in stanza ids
python-nbxmpp 0.5.3 (13 July 2015)
* Fix receiving long utf8 strings under py3
* Fix issue with pyopenssl 0.15.1
* Fix decoding issues
python-nbxmpp 0.5.2 (27 December 2014)
* Fix BOSH HTTP requests
* Fix handling of binary SASL data for mechanism GSSAPI
* Update MAM namespace
python-nbxmpp 0.5.1 (04 October 2014)
* Fix printing network errors in a non-utf-8 console
python-nbxmpp 0.5 (02 July 2014)
* support both python2 and python3
* Fix storing server certificate when there is no error
python-nbxmpp 0.4 (15 March 2014)
* Ability to configure TLS protocol version
* Add support for SCRAM-SHA-1-PLUS
* Security improvements
python-nbxmpp 0.3 (23 December 2013)
* Improve security level
* Ability to configure cipher list
* Store only depth 0 SSL certificate
python-nbxmpp 0.2 (26 July 2013)
* Add some namespace
* do TLS handshake without blocking
* store all SSL errors instead of only last one
python-nbxmpp 0.1 (05 August 2012)
* Initial release
python-nbxmpp-nbxmpp-2.0.4/MANIFEST.in 0000664 0000000 0000000 00000000127 14130341156 0017376 0 ustar 00root root 0000000 0000000 include ChangeLog COPYING README
recursive-include doc *
recursive-include nbxmpp *.py
python-nbxmpp-nbxmpp-2.0.4/README.md 0000664 0000000 0000000 00000001553 14130341156 0017123 0 ustar 00root root 0000000 0000000 # Welcome to python-nbxmpp
`python-nbxmpp` is a Python library that provides a way for Python applications to use the XMPP network. This library was initially a fork of `xmpppy`.
## Runtime Requirements
- python >= 3.7.0
- PyGObject
- GLib >= 2.60
- libsoup
- precis-i18n
- idna
## Optional Runtime Requirements
- python-gssapi (for GSSAPI authentication https://pypi.org/project/gssapi/)
## Features
* List of [supported XEPs](https://dev.gajim.org/gajim/python-nbxmpp/-/wikis/Supported-XEPs-in-python-nbxmpp/)
## Starting Points
* [Downloads](https://dev.gajim.org/gajim/python-nbxmpp/tags)
* You can also clone the [git repository](https://dev.gajim.org/gajim/python-nbxmpp.git)
### Setup
Run the following:
pip install .
### Usage
To use python-nbxmpp, `import nbxmpp` in your application.
or use the example client `python3 -m nbxmpp.examples.client`
python-nbxmpp-nbxmpp-2.0.4/mypy.ini 0000664 0000000 0000000 00000000375 14130341156 0017344 0 ustar 00root root 0000000 0000000 [mypy]
python_version = 3.7
warn_unused_configs = True
disallow_incomplete_defs = True
allow_redefinition = True
[mypy-gi.*]
ignore_missing_imports = True
[mypy-precis_i18n.*]
ignore_missing_imports = True
[mypy-idna.*]
ignore_missing_imports = True
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/ 0000775 0000000 0000000 00000000000 14130341156 0017144 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/__init__.py 0000664 0000000 0000000 00000000135 14130341156 0021254 0 ustar 00root root 0000000 0000000 import gi
from .protocol import *
gi.require_version('Soup', '2.4')
__version__ = "2.0.4"
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/addresses.py 0000664 0000000 0000000 00000016350 14130341156 0021500 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from collections import namedtuple
from nbxmpp.util import Observable
from nbxmpp.resolver import GioResolver
from nbxmpp.const import ConnectionType
from nbxmpp.const import ConnectionProtocol
log = logging.getLogger('nbxmpp.addresses')
class ServerAddress(namedtuple('ServerAddress', 'domain service host uri '
'protocol type proxy')):
__slots__ = []
@property
def is_service(self):
return self.service is not None
@property
def is_host(self):
return self.host is not None
@property
def is_uri(self):
return self.uri is not None
def has_proxy(self):
return self.proxy is not None
class ServerAddresses(Observable):
'''
Signals:
resolved
'''
def __init__(self, domain):
Observable.__init__(self, log)
self._domain = domain
self._custom_host = None
self._proxy = None
self._is_resolved = False
self._addresses = [
ServerAddress(domain=self._domain,
service='xmpps-client',
host=None,
uri=None,
protocol=ConnectionProtocol.TCP,
type=ConnectionType.DIRECT_TLS,
proxy=None),
ServerAddress(domain=self._domain,
service='xmpp-client',
host=None,
uri=None,
protocol=ConnectionProtocol.TCP,
type=ConnectionType.START_TLS,
proxy=None),
ServerAddress(domain=self._domain,
service='xmpp-client',
host=None,
uri=None,
protocol=ConnectionProtocol.TCP,
type=ConnectionType.PLAIN,
proxy=None)
]
self._fallback_addresses = [
ServerAddress(domain=self._domain,
service=None,
host='%s:%s' % (self._domain, 5222),
uri=None,
protocol=ConnectionProtocol.TCP,
type=ConnectionType.START_TLS,
proxy=None),
ServerAddress(domain=self._domain,
service=None,
host='%s:%s' % (self._domain, 5222),
uri=None,
protocol=ConnectionProtocol.TCP,
type=ConnectionType.PLAIN,
proxy=None)
]
@property
def domain(self):
return self._domain
@property
def is_resolved(self):
return self._is_resolved
def resolve(self):
if self._is_resolved:
self._on_request_resolved()
return
if self._proxy is not None:
# Let the proxy resolve the domain
self._on_request_resolved()
return
if self._custom_host is not None:
self._on_request_resolved()
return
GioResolver().resolve_alternatives(self._domain,
self._on_alternatives_result)
def cancel_resolve(self):
self.remove_subscriptions()
def set_custom_host(self, address):
# Set a custom host, overwrites all other addresses
self._custom_host = address
if address is None:
return
host_or_uri, protocol, type_ = address
if protocol == ConnectionProtocol.WEBSOCKET:
host, uri = None, host_or_uri
else:
host, uri = host_or_uri, None
self._fallback_addresses = []
self._addresses = [
ServerAddress(domain=self._domain,
service=None,
host=host,
uri=uri,
protocol=protocol,
type=type_,
proxy=None)]
def set_proxy(self, proxy):
self._proxy = proxy
def _on_alternatives_result(self, uri):
if uri is None:
self._on_request_resolved()
return
if uri.startswith('wss'):
type_ = ConnectionType.DIRECT_TLS
elif uri.startswith('ws'):
type_ = ConnectionType.PLAIN
else:
log.warning('Invalid websocket uri: %s', uri)
self._on_request_resolved()
return
addr = ServerAddress(domain=self._domain,
service=None,
host=None,
uri=uri,
protocol=ConnectionProtocol.WEBSOCKET,
type=type_,
proxy=None)
self._addresses.append(addr)
self._on_request_resolved()
def _on_request_resolved(self):
self._is_resolved = True
self.notify('resolved')
self.remove_subscriptions()
def get_next_address(self,
allowed_types,
allowed_protocols):
'''
Selects next address
'''
for addr in self._filter_allowed(self._addresses,
allowed_types,
allowed_protocols):
yield self._assure_proxy(addr)
for addr in self._filter_allowed(self._fallback_addresses,
allowed_types,
allowed_protocols):
yield self._assure_proxy(addr)
raise NoMoreAddresses
def _assure_proxy(self, addr):
if self._proxy is None:
return addr
if addr.protocol == ConnectionProtocol.TCP:
return addr._replace(proxy=self._proxy)
return addr
def _filter_allowed(self, addresses, allowed_types, allowed_protocols):
if self._proxy is not None:
addresses = filter(lambda addr: addr.host is not None, addresses)
addresses = filter(lambda addr: addr.type in allowed_types,
addresses)
addresses = filter(lambda addr: addr.protocol in allowed_protocols,
addresses)
return addresses
def __str__(self):
addresses = self._addresses + self._fallback_addresses
return '\n'.join([str(addr) for addr in addresses])
class NoMoreAddresses(Exception):
pass
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/auth.py 0000664 0000000 0000000 00000034651 14130341156 0020470 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import os
import hmac
import binascii
import logging
import hashlib
from hashlib import pbkdf2_hmac
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.protocol import SASL_ERROR_CONDITIONS
from nbxmpp.protocol import SASL_AUTH_MECHS
from nbxmpp.util import b64decode
from nbxmpp.util import b64encode
from nbxmpp.util import LogAdapter
from nbxmpp.const import StreamState
log = logging.getLogger('nbxmpp.auth')
try:
gssapi = __import__('gssapi')
GSSAPI_AVAILABLE = True
except (ImportError, OSError) as error:
log.warning('GSSAPI not available: %s', error)
GSSAPI_AVAILABLE = False
class SASL:
"""
Implements SASL authentication.
"""
def __init__(self, client):
self._client = client
self._password = None
self._allowed_mechs = None
self._enabled_mechs = None
self._method = None
self._error = None
self._log = LogAdapter(log, {'context': client.log_context})
@property
def error(self):
return self._error
def set_password(self, password):
self._password = password
@property
def password(self):
return self._password
def delegate(self, stanza):
if stanza.getNamespace() != Namespace.SASL:
return
if stanza.getName() == 'challenge':
self._on_challenge(stanza)
elif stanza.getName() == 'failure':
self._on_failure(stanza)
elif stanza.getName() == 'success':
self._on_success(stanza)
def start_auth(self, features):
self._allowed_mechs = self._client.mechs
self._enabled_mechs = self._allowed_mechs
self._method = None
self._error = None
# -PLUS variants need TLS channel binding data
# This is currently not supported via GLib
self._enabled_mechs.discard('SCRAM-SHA-1-PLUS')
self._enabled_mechs.discard('SCRAM-SHA-256-PLUS')
# channel_binding_data = None
if not GSSAPI_AVAILABLE:
self._enabled_mechs.discard('GSSAPI')
available_mechs = features.get_mechs() & self._enabled_mechs
self._log.info('Available mechanisms: %s', available_mechs)
domain_based_name = features.get_domain_based_name()
if domain_based_name is not None:
self._log.info('Found domain based name: %s', domain_based_name)
if not available_mechs:
self._log.error('No available auth mechanisms found')
self._abort_auth('invalid-mechanism')
return
chosen_mechanism = None
for mech in SASL_AUTH_MECHS:
if mech in available_mechs:
chosen_mechanism = mech
break
if chosen_mechanism is None:
self._log.error('No available auth mechanisms found')
self._abort_auth('invalid-mechanism')
return
self._log.info('Chosen auth mechanism: %s', chosen_mechanism)
if chosen_mechanism in ('SCRAM-SHA-256', 'SCRAM-SHA-1', 'PLAIN'):
if not self._password:
self._on_sasl_finished(False, 'no-password')
return
# if chosen_mechanism == 'SCRAM-SHA-256-PLUS':
# self._method = SCRAM_SHA_256_PLUS(self._client,
# channel_binding_data)
# self._method.initiate(self._client.username, self._password)
# elif chosen_mechanism == 'SCRAM-SHA-1-PLUS':
# self._method = SCRAM_SHA_1_PLUS(self._client,
# channel_binding_data)
# self._method.initiate(self._client.username, self._password)
if chosen_mechanism == 'SCRAM-SHA-256':
self._method = SCRAM_SHA_256(self._client, None)
self._method.initiate(self._client.username, self._password)
elif chosen_mechanism == 'SCRAM-SHA-1':
self._method = SCRAM_SHA_1(self._client, None)
self._method.initiate(self._client.username, self._password)
elif chosen_mechanism == 'PLAIN':
self._method = PLAIN(self._client)
self._method.initiate(self._client.username, self._password)
elif chosen_mechanism == 'ANONYMOUS':
self._method = ANONYMOUS(self._client)
self._method.initiate()
elif chosen_mechanism == 'EXTERNAL':
self._method = EXTERNAL(self._client)
self._method.initiate(self._client.username, self._client.Server)
elif chosen_mechanism == 'GSSAPI':
self._method = GSSAPI(self._client)
if domain_based_name:
hostname = domain_based_name
else:
hostname = self._client.domain
try:
self._method.initiate(hostname)
except AuthFail as error:
self._log.error(error)
self._abort_auth()
return
else:
self._log.error('Unknown auth mech')
def _on_challenge(self, stanza):
try:
self._method.response(stanza.getData())
except AttributeError:
self._log.info('Mechanism has no response method')
self._abort_auth()
except AuthFail as error:
self._log.error(error)
self._abort_auth()
def _on_success(self, stanza):
self._log.info('Successfully authenticated with remote server')
try:
self._method.success(stanza.getData())
except AttributeError:
pass
except AuthFail as error:
self._log.error(error)
self._abort_auth()
return
self._on_sasl_finished(True, None, None)
def _on_failure(self, stanza):
text = stanza.getTagData('text')
reason = 'not-authorized'
childs = stanza.getChildren()
for child in childs:
name = child.getName()
if name == 'text':
continue
if name in SASL_ERROR_CONDITIONS:
reason = name
break
self._log.info('Failed SASL authentification: %s %s', reason, text)
self._abort_auth(reason, text)
def _abort_auth(self, reason='malformed-request', text=None):
node = Node('abort', attrs={'xmlns': Namespace.SASL})
self._client.send_nonza(node)
self._on_sasl_finished(False, reason, text)
def _on_sasl_finished(self, successful, reason, text=None):
if not successful:
self._error = (reason, text)
self._client.set_state(StreamState.AUTH_FAILED)
else:
self._client.set_state(StreamState.AUTH_SUCCESSFUL)
class PLAIN:
_mechanism = 'PLAIN'
def __init__(self, client):
self._client = client
def initiate(self, username, password):
payload = b64encode('\x00%s\x00%s' % (username, password))
node = Node('auth',
attrs={'xmlns': Namespace.SASL, 'mechanism': 'PLAIN'},
payload=[payload])
self._client.send_nonza(node)
class EXTERNAL:
_mechanism = 'EXTERNAL'
def __init__(self, client):
self._client = client
def initiate(self, username, server):
payload = b64encode('%s@%s' % (username, server))
node = Node('auth',
attrs={'xmlns': Namespace.SASL, 'mechanism': 'EXTERNAL'},
payload=[payload])
self._client.send_nonza(node)
class ANONYMOUS:
_mechanism = 'ANONYMOUS'
def __init__(self, client):
self._client = client
def initiate(self):
node = Node('auth', attrs={'xmlns': Namespace.SASL,
'mechanism': 'ANONYMOUS'})
self._client.send_nonza(node)
class GSSAPI:
# See https://tools.ietf.org/html/rfc4752#section-3.1
_mechanism = 'GSSAPI'
def __init__(self, client):
self._client = client
def initiate(self, hostname):
service = gssapi.Name(
'xmpp@%s' % hostname, name_type=gssapi.NameType.hostbased_service)
try:
self.ctx = gssapi.SecurityContext(
name=service, usage="initiate",
flags=gssapi.RequirementFlag.integrity)
token = self.ctx.step()
except (gssapi.exceptions.GeneralError, gssapi.raw.misc.GSSError) as e:
raise AuthFail(e)
node = Node('auth',
attrs={'xmlns': Namespace.SASL, 'mechanism': 'GSSAPI'},
payload=b64encode(token))
self._client.send_nonza(node)
def response(self, server_message, *args, **kwargs):
server_message = b64decode(server_message, bytes)
try:
if not self.ctx.complete:
output_token = self.ctx.step(server_message)
else:
result = self.ctx.unwrap(server_message)
# TODO(jelmer): Log result.message
data = b'\x00\x00\x00\x00' + bytes(self.ctx.initiator_name)
output_token = self.ctx.wrap(data, False).message
except (gssapi.exceptions.GeneralError, gssapi.raw.misc.GSSError) as e:
raise AuthFail(e)
response = b64encode(output_token)
node = Node('response',
attrs={'xmlns': Namespace.SASL},
payload=response)
self._client.send_nonza(node)
class SCRAM:
_mechanism = ''
_channel_binding = ''
_hash_method = ''
def __init__(self, client, channel_binding):
self._client = client
self._channel_binding_data = channel_binding
self._client_nonce = '%x' % int(binascii.hexlify(os.urandom(24)), 16)
self._client_first_message_bare = None
self._server_signature = None
self._password = None
@property
def nonce_length(self):
return len(self._client_nonce)
@property
def _b64_channel_binding_data(self):
if self._mechanism.endswith('PLUS'):
return b64encode(b'%s%s' % (self._channel_binding.encode(),
self._channel_binding_data))
return b64encode(self._channel_binding)
@staticmethod
def _scram_parse(scram_data):
return dict(s.split('=', 1) for s in scram_data.split(','))
def initiate(self, username, password):
self._password = password
self._client_first_message_bare = 'n=%s,r=%s' % (username,
self._client_nonce)
client_first_message = '%s%s' % (self._channel_binding,
self._client_first_message_bare)
payload = b64encode(client_first_message)
node = Node('auth',
attrs={'xmlns': Namespace.SASL,
'mechanism': self._mechanism},
payload=[payload])
self._client.send_nonza(node)
def response(self, server_first_message):
server_first_message = b64decode(server_first_message)
challenge = self._scram_parse(server_first_message)
client_nonce = challenge['r'][:self.nonce_length]
if client_nonce != self._client_nonce:
raise AuthFail('Invalid client nonce received from server')
salt = b64decode(challenge['s'], bytes)
iteration_count = int(challenge['i'])
if iteration_count < 4096:
raise AuthFail('Salt iteration count to low: %s' % iteration_count)
salted_password = pbkdf2_hmac(self._hash_method,
self._password.encode('utf8'),
salt,
iteration_count)
client_final_message_wo_proof = 'c=%s,r=%s' % (
self._b64_channel_binding_data,
challenge['r']
)
client_key = self._hmac(salted_password, 'Client Key')
stored_key = self._h(client_key)
auth_message = '%s,%s,%s' % (self._client_first_message_bare,
server_first_message,
client_final_message_wo_proof)
client_signature = self._hmac(stored_key, auth_message)
client_proof = self._xor(client_key, client_signature)
client_finale_message = 'c=%s,r=%s,p=%s' % (
self._b64_channel_binding_data,
challenge['r'],
b64encode(client_proof)
)
server_key = self._hmac(salted_password, 'Server Key')
self._server_signature = self._hmac(server_key, auth_message)
payload = b64encode(client_finale_message)
node = Node('response',
attrs={'xmlns': Namespace.SASL},
payload=[payload])
self._client.send_nonza(node)
def success(self, server_last_message):
server_last_message = b64decode(server_last_message)
success = self._scram_parse(server_last_message)
server_signature = b64decode(success['v'], bytes)
if server_signature != self._server_signature:
raise AuthFail('Invalid server signature')
def _hmac(self, key, message):
return hmac.new(key=key,
msg=message.encode(),
digestmod=self._hash_method).digest()
@staticmethod
def _xor(x, y):
return bytes([px ^ py for px, py in zip(x, y)])
def _h(self, data):
return hashlib.new(self._hash_method, data).digest()
class SCRAM_SHA_1(SCRAM):
_mechanism = 'SCRAM-SHA-1'
_channel_binding = 'n,,'
_hash_method = 'sha1'
class SCRAM_SHA_1_PLUS(SCRAM_SHA_1):
_mechanism = 'SCRAM-SHA-1-PLUS'
_channel_binding = 'p=tls-unique,,'
class SCRAM_SHA_256(SCRAM):
_mechanism = 'SCRAM-SHA-256'
_channel_binding = 'n,,'
_hash_method = 'sha256'
class SCRAM_SHA_256_PLUS(SCRAM_SHA_256):
_mechanism = 'SCRAM-SHA-256-PLUS'
_channel_binding = 'p=tls-unique,,'
class AuthFail(Exception):
pass
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/c14n.py 0000664 0000000 0000000 00000003651 14130341156 0020270 0 ustar 00root root 0000000 0000000 ## c14n.py
##
## Copyright (C) 2007-2008 Brendan Taylor
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see .
##
"""
XML canonicalisation methods (for XEP-0116)
"""
def c14n(node, is_buggy):
s = "<" + node.name
if node.namespace:
if not node.parent or node.parent.namespace != node.namespace:
s += ' xmlns="%s"' % node.namespace
sorted_attrs = sorted(node.attrs.keys())
for key in sorted_attrs:
if not is_buggy and key == 'xmlns':
continue
val = str(node.attrs[key])
# like XMLescape() but with whitespace and without >
s += ' %s="%s"' % (key, normalise_attr(val))
s += ">"
cnt = 0
if node.kids:
for a in node.kids:
if (len(node.data)-1) >= cnt:
s = s + normalise_text(node.data[cnt])
s = s + c14n(a, is_buggy)
cnt += 1
if (len(node.data)-1) >= cnt:
s = s + normalise_text(node.data[cnt])
if not node.kids and s.endswith('>'):
s=s[:-1]+' />'
else:
s = s + "" + node.name + ">"
return s
def normalise_attr(val):
return val.replace('&', '&').replace('<', '<').replace('"', '"').replace('\t', ' ').replace('\n', '
').replace('\r', '
')
def normalise_text(val):
return val.replace('&', '&').replace('<', '<').replace('>', '>').replace('\r', '
')
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/client.py 0000664 0000000 0000000 00000071007 14130341156 0021001 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from gi.repository import GLib
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Features
from nbxmpp.protocol import StanzaMalformed
from nbxmpp.protocol import SessionRequest
from nbxmpp.protocol import BindRequest
from nbxmpp.protocol import TLSRequest
from nbxmpp.protocol import isResultNode
from nbxmpp.protocol import JID
from nbxmpp.protocol import Protocol
from nbxmpp.protocol import WebsocketCloseHeader
from nbxmpp.errors import TimeoutStanzaError
from nbxmpp.errors import StanzaError
from nbxmpp.errors import CancelledError
from nbxmpp.addresses import ServerAddresses
from nbxmpp.addresses import NoMoreAddresses
from nbxmpp.tcp import TCPConnection
from nbxmpp.websocket import WebsocketConnection
from nbxmpp.smacks import Smacks
from nbxmpp.auth import SASL
from nbxmpp.const import StreamState
from nbxmpp.const import StreamError
from nbxmpp.const import ConnectionType
from nbxmpp.const import ConnectionProtocol
from nbxmpp.const import Mode
from nbxmpp.dispatcher import StanzaDispatcher
from nbxmpp.util import get_stream_header
from nbxmpp.util import get_stanza_id
from nbxmpp.util import Observable
from nbxmpp.util import validate_stream_header
from nbxmpp.util import LogAdapter
log = logging.getLogger('nbxmpp.stream')
class Client(Observable):
def __init__(self, log_context=None):
'''
Signals:
resume-failed
resume-successful
login-successful
anonymous-supported
disconnected
connected
connection-failed
stanza-sent
stanza-received
'''
self._log_context = log_context
if log_context is None:
self._log_context = str(id(self))
self._log = LogAdapter(log, {'context': self._log_context})
Observable.__init__(self, self._log)
self._jid = None
self._lang = 'en'
self._domain = None
self._username = None
self._resource = None
self._custom_host = None
self._addresses = None
self._current_address = None
self._address_generator = None
self._client_cert = None
self._client_cert_pass = None
self._proxy = None
self._allowed_con_types = None
self._allowed_protocols = None
self._allowed_mechs = None
self._sm_disabled = False
self._stream_id = None
self._stream_secure = False
self._stream_authenticated = False
self._stream_features = None
self._session_required = False
self._connect_successful = False
self._stream_close_initiated = False
self._ping_task = None
self._error = None, None, None
self._ignored_tls_errors = set()
self._ignore_tls_errors = False
self._accepted_certificates = []
self._peer_certificate = None
self._peer_certificate_errors = None
self._con = None
self._local_address = None
self._remote_address = None
self._mode = Mode.CLIENT
self._ping_source_id = None
self._tasks = []
self._dispatcher = StanzaDispatcher(self)
self._dispatcher.subscribe('before-dispatch', self._on_before_dispatch)
self._dispatcher.subscribe('parsing-error', self._on_parsing_error)
self._dispatcher.subscribe('stream-end', self._on_stream_end)
self._smacks = Smacks(self)
self._sasl = SASL(self)
self._state = StreamState.DISCONNECTED
def add_task(self, task):
self._tasks.append(task)
def remove_task(self, task, _context):
try:
self._tasks.remove(task)
except Exception:
pass
@property
def log_context(self):
return self._log_context
@property
def features(self):
return self._stream_features
@property
def sm_supported(self):
return self._smacks.sm_supported
@property
def lang(self):
return self._lang
@property
def username(self):
return self._username
@property
def domain(self):
return self._domain
@property
def resource(self):
return self._resource
def set_username(self, username):
self._username = username
def set_domain(self, domain):
self._domain = domain
def set_resource(self, resource):
self._resource = resource
def set_mode(self, mode):
self._mode = mode
@property
def custom_host(self):
return self._custom_host
def set_custom_host(self, host_or_uri, protocol, type_):
if self._domain is None:
raise ValueError('Call set_domain() first before set_custom_host()')
self._custom_host = (host_or_uri, protocol, type_)
def set_accepted_certificates(self, certificates):
self._accepted_certificates = certificates
@property
def ignored_tls_errors(self):
return self._ignored_tls_errors
def set_ignored_tls_errors(self, errors):
if errors is None:
errors = set()
self._ignored_tls_errors = errors
@property
def ignore_tls_errors(self):
return self._ignore_tls_errors
def set_ignore_tls_errors(self, ignore):
self._ignore_tls_errors = ignore
def set_password(self, password):
self._sasl.set_password(password)
@property
def password(self):
return self._sasl.password
@property
def peer_certificate(self):
return self._peer_certificate, self._peer_certificate_errors
@property
def current_address(self):
return self._current_address
@property
def current_connection_type(self):
return self._current_address.type
@property
def is_websocket(self):
return self._current_address.protocol == ConnectionProtocol.WEBSOCKET
@property
def stream_id(self):
return self._stream_id
@property
def is_stream_secure(self):
direct_tls = self.current_connection_type == ConnectionType.DIRECT_TLS
return self._stream_secure or direct_tls
@property
def is_stream_authenticated(self):
return self._stream_authenticated
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self._log.info('Set state: %s', value)
def set_state(self, state):
self.state = state
self._xmpp_state_machine()
@property
def local_address(self):
return self._local_address
@property
def remote_address(self):
return self._remote_address
@property
def connection_types(self):
if self._custom_host is not None:
return [self._custom_host[2]]
return list(self._allowed_con_types or [ConnectionType.DIRECT_TLS,
ConnectionType.START_TLS])
def set_connection_types(self, con_types):
self._allowed_con_types = con_types
@property
def mechs(self):
return set(self._allowed_mechs or set(['SCRAM-SHA-256',
'SCRAM-SHA-1',
'PLAIN']))
def set_mechs(self, mechs):
self._allowed_mechs = mechs
@property
def protocols(self):
if self._custom_host is not None:
return [self._custom_host[1]]
return list(self._allowed_protocols or [ConnectionProtocol.TCP,
ConnectionProtocol.WEBSOCKET])
def set_protocols(self, protocols):
self._allowed_protocols = protocols
def set_sm_disabled(self, value):
self._sm_disabled = value
@property
def sm_disabled(self):
return self._sm_disabled
@property
def client_cert(self):
return self._client_cert, self._client_cert_pass
def set_client_cert(self, client_cert, client_cert_pass):
self._client_cert = client_cert
self._client_cert_pass = client_cert_pass
def set_proxy(self, proxy):
self._proxy = proxy
self._dispatcher.get_module('Muclumbus').set_proxy(proxy)
@property
def proxy(self):
return self._proxy
def get_bound_jid(self):
return self._jid
def _set_bound_jid(self, jid):
self._jid = JID.from_string(jid)
@property
def has_error(self):
return self._error[0] is not None
def get_error(self):
return self._error
def _reset_error(self):
self._error = None, None, None
def _set_error(self, domain, error, text=None):
self._log.info('Set error: %s, %s, %s', domain, error, text)
self._error = domain, error, text
def _connect(self):
if self._state not in (StreamState.DISCONNECTED, StreamState.RESOLVED):
self._log.error('Stream can\'t connect, stream state: %s',
self._state)
return
self.state = StreamState.CONNECTING
self._reset_error()
self._con = self._get_connection(self._log_context,
self._current_address,
self._accepted_certificates,
self._ignore_tls_errors,
self._ignored_tls_errors,
self.client_cert)
self._con.subscribe('connected', self._on_connected)
self._con.subscribe('connection-failed', self._on_connection_failed)
self._con.subscribe('disconnected', self._on_disconnected)
self._con.subscribe('data-sent', self._on_data_sent)
self._con.subscribe('data-received', self._on_data_received)
self._con.subscribe('bad-certificate', self._on_bad_certificate)
self._con.subscribe('certificate-set', self._on_certificate_set)
self._con.connect()
def _get_connection(self, *args):
if self.is_websocket:
return WebsocketConnection(*args)
return TCPConnection(*args)
def connect(self):
if self._state != StreamState.DISCONNECTED:
self._log.error('Stream can\'t reconnect, stream state: %s',
self._state)
return
if self._connect_successful:
self._log.info('Reconnect')
self._connect()
return
self._log.info('Connect')
self._reset_error()
self.state = StreamState.RESOLVE
self._addresses = ServerAddresses(self._domain)
self._addresses.set_custom_host(self._custom_host)
self._addresses.set_proxy(self._proxy)
self._addresses.subscribe('resolved', self._on_addresses_resolved)
self._addresses.resolve()
def _on_addresses_resolved(self, _addresses, _signal_name):
self._log.info('Domain resolved')
self._log.info(self._addresses)
self.state = StreamState.RESOLVED
self._address_generator = self._addresses.get_next_address(
self.connection_types,
self.protocols)
self._try_next_ip()
def _try_next_ip(self, *args):
try:
self._current_address = next(self._address_generator)
except NoMoreAddresses:
self._current_address = None
self.state = StreamState.DISCONNECTED
self._log.error('Unable to connect to %s', self._addresses.domain)
self._set_error(StreamError.CONNECTION_FAILED,
'connection-failed',
'Unable to connect to %s' % self._addresses.domain)
self.notify('connection-failed')
return
self._log.info('Current address: %s', self._current_address)
self._connect()
def disconnect(self, immediate=False):
if self._state == StreamState.RESOLVE:
self._addresses.cancel_resolve()
self.state = StreamState.DISCONNECTED
return
if self._state == StreamState.CONNECTING:
self._disconnect()
return
if self._state in (StreamState.DISCONNECTED,
StreamState.DISCONNECTING):
self._log.warning('Stream can\'t disconnect, stream state: %s',
self._state)
return
self._disconnect(immediate=immediate)
def _disconnect(self, immediate=True):
self.state = StreamState.DISCONNECTING
self._remove_ping_timer()
self._cancel_ping_task()
if not immediate:
self._stream_close_initiated = True
self._smacks.close_session()
self._end_stream()
self._con.shutdown_output()
else:
self._con.disconnect()
def send(self, stanza, *args, **kwargs):
# Alias for backwards compat
return self.send_stanza(stanza)
def _on_connected(self, connection, _signal_name):
self.set_state(StreamState.CONNECTED)
self._local_address = connection.local_address
self._remote_address = connection.remote_address
def _on_disconnected(self, _connection, _signal_name):
self.state = StreamState.DISCONNECTED
for task in self._tasks:
task.cancel()
self._remove_ping_timer()
self._cancel_ping_task()
self._reset_stream()
self.notify('disconnected')
def _on_connection_failed(self, _connection, _signal_name):
self.state = StreamState.DISCONNECTED
self._reset_stream()
if not self._connect_successful:
self._try_next_ip()
else:
self._set_error(StreamError.CONNECTION_FAILED,
'connection-failed',
(f'Unable to connect to last '
'successful address: {self._current_address}'))
self.notify('connection-failed')
def _disconnect_with_error(self, error_domain, error, text=None):
self._set_error(error_domain, error, text)
self.disconnect()
def _on_parsing_error(self, _dispatcher, _signal_name, error):
if self._state == StreamState.DISCONNECTING:
# Don't notify about parsing errors if we already ended the stream
return
self._disconnect_with_error(StreamError.PARSING, 'parsing-error', error)
def _on_stream_end(self, _dispatcher, _signal_name, error):
if not self.has_error:
self._set_error(StreamError.STREAM, error or 'stream-end')
self._con.shutdown_input()
if not self._stream_close_initiated:
self.state = StreamState.DISCONNECTING
self._remove_ping_timer()
self._cancel_ping_task()
self._smacks.close_session()
self._end_stream()
self._con.shutdown_output()
def _reset_stream(self):
self._stream_id = None
self._stream_secure = False
self._stream_authenticated = False
self._stream_features = None
self._session_required = False
self._con = None
def _end_stream(self):
if self.is_websocket:
nonza = WebsocketCloseHeader()
else:
nonza = ''
self.send_nonza(nonza)
def get_module(self, name):
return self._dispatcher.get_module(name)
def _on_bad_certificate(self, connection, _signal_name):
self._peer_certificate, self._peer_certificate_errors = \
connection.peer_certificate
self._set_error(StreamError.BAD_CERTIFICATE, 'bad certificate')
def _on_certificate_set(self, connection, _signal_name):
self._peer_certificate, self._peer_certificate_errors = \
connection.peer_certificate
def accept_certificate(self):
self._log.info('Certificate accepted')
self._accepted_certificates.append(self._peer_certificate)
self._connect()
def _on_data_sent(self, _connection, _signal_name, data):
self.notify('stanza-sent', data)
def _on_before_dispatch(self, _dispatcher, _signal_name, data):
self.notify('stanza-received', data)
def _on_data_received(self, _connection, _signal_name, data):
self._dispatcher.process_data(data)
self._reset_ping_timer()
def _reset_ping_timer(self):
if self.is_websocket:
return
if not self._mode.is_client:
return
if self.state != StreamState.ACTIVE:
return
if self._ping_source_id is not None:
self._log.info('Remove ping timer')
GLib.source_remove(self._ping_source_id)
self._ping_source_id = None
self._log.info('Start ping timer')
self._ping_source_id = GLib.timeout_add_seconds(180, self._ping)
def _remove_ping_timer(self):
if self._ping_source_id is None:
return
self._log.info('Remove ping timer')
GLib.source_remove(self._ping_source_id)
self._ping_source_id = None
def send_stanza(self, stanza, now=False, callback=None,
timeout=None, user_data=None):
if user_data is not None and not isinstance(user_data, dict):
raise ValueError('arg user_data must be of dict type')
if not isinstance(stanza, Protocol):
raise ValueError('Nonzas not allowed, use send_nonza()')
id_ = stanza.getID()
if id_ is None:
id_ = get_stanza_id()
stanza.setID(id_)
if callback is not None:
self._dispatcher.add_callback_for_id(
id_, callback, timeout, user_data)
self._con.send(stanza, now)
self._smacks.save_in_queue(stanza)
return id_
def SendAndCallForResponse(self, stanza, callback, user_data=None):
self.send_stanza(stanza, callback=callback, user_data=user_data)
def send_nonza(self, nonza, now=False):
self._con.send(nonza, now)
def _xmpp_state_machine(self, stanza=None):
self._log.info('Execute state machine')
if stanza is not None:
if stanza.getName() == 'error':
self._log.info('Stream error')
# TODO:
# self._disconnect_with_error(StreamError.SASL,
# stanza.get_condition())
return
if self.state == StreamState.CONNECTED:
self._dispatcher.set_dispatch_callback(self._xmpp_state_machine)
if (self.current_connection_type == ConnectionType.DIRECT_TLS and
not self.is_websocket):
self._con.start_tls_negotiation()
self._stream_secure = True
self._start_stream()
return
self._start_stream()
elif self.state == StreamState.WAIT_FOR_STREAM_START:
try:
self._stream_id = validate_stream_header(stanza,
self._domain,
self.is_websocket)
except StanzaMalformed as error:
self._log.error(error)
self._disconnect_with_error(StreamError.STREAM,
'stanza-malformed',
'Invalid stream header')
return
if (self._stream_secure or
self.current_connection_type == ConnectionType.PLAIN):
# TLS Negotiation succeeded or we are connected PLAIN
# We received the stream header and consider this as
# successfully connected, this means we will not try
# other connection methods if an error happensafterwards
self._connect_successful = True
self.state = StreamState.WAIT_FOR_FEATURES
elif self.state == StreamState.WAIT_FOR_FEATURES:
if stanza.getName() != 'features':
self._log.error('Invalid response: %s', stanza)
self._disconnect_with_error(
StreamError.STREAM,
'stanza-malformed',
'Invalid response, expected features')
return
self._on_stream_features(Features(stanza))
elif self.state == StreamState.WAIT_FOR_TLS_PROCEED:
if stanza.getNamespace() != Namespace.TLS:
self._disconnect_with_error(
StreamError.TLS,
'stanza-malformed',
'Invalid namespace for TLS response')
return
if stanza.getName() == 'failure':
self._disconnect_with_error(StreamError.TLS,
'negotiation-failed')
return
if stanza.getName() == 'proceed':
self._con.start_tls_negotiation()
self._stream_secure = True
self._start_stream()
return
self._log.error('Invalid response')
self._disconnect_with_error(StreamError.TLS,
'stanza-malformed',
'Invalid TLS response')
return
elif self.state == StreamState.PROCEED_WITH_AUTH:
self._sasl.delegate(stanza)
elif self.state == StreamState.AUTH_SUCCESSFUL:
self._stream_authenticated = True
if self._mode.is_login_test:
self.notify('login-successful')
# Reset parser because we will receive a new stream header
# which will otherwise lead to a parsing error
self._dispatcher.reset_parser()
self.disconnect()
return
self._start_stream()
elif self.state == StreamState.AUTH_FAILED:
self._disconnect_with_error(StreamError.SASL,
*self._sasl.error)
elif self.state == StreamState.WAIT_FOR_BIND:
self._on_bind(stanza)
elif self.state == StreamState.BIND_SUCCESSFUL:
self._dispatcher.clear_iq_callbacks()
self._dispatcher.set_dispatch_callback(None)
self._smacks.send_enable()
self.state = StreamState.ACTIVE
self.notify('connected')
elif self.state == StreamState.WAIT_FOR_SESSION:
self._on_session(stanza)
elif self.state == StreamState.WAIT_FOR_RESUMED:
self._smacks.delegate(stanza)
elif self.state == StreamState.RESUME_FAILED:
self.notify('resume-failed')
self._start_bind()
elif self.state == StreamState.RESUME_SUCCESSFUL:
self._dispatcher.set_dispatch_callback(None)
self.state = StreamState.ACTIVE
self.notify('resume-successful')
def _on_stream_features(self, features):
if self.is_stream_authenticated:
self._stream_features = features
self._smacks.sm_supported = features.has_sm()
self._session_required = features.session_required()
if self._smacks.resume_supported:
self._smacks.resume_request()
self.state = StreamState.WAIT_FOR_RESUMED
else:
self._start_bind()
elif self.is_stream_secure:
if self._mode.is_register:
if features.has_register():
self.state = StreamState.ACTIVE
self._dispatcher.set_dispatch_callback(None)
self.notify('connected')
else:
self._disconnect_with_error(StreamError.REGISTER,
'register-not-supported')
return
if self._mode.is_anonymous_test:
if features.has_anonymous():
self.notify('anonymous-supported')
self.disconnect()
else:
self._disconnect_with_error(StreamError.SASL,
'anonymous-not-supported')
return
self._start_auth(features)
else:
tls_supported, required = features.has_starttls()
if self._current_address.type == ConnectionType.PLAIN:
if tls_supported and required:
self._log.error('Server requires TLS')
self._disconnect_with_error(StreamError.TLS, 'tls-required')
return
self._start_auth(features)
return
if not tls_supported:
self._log.error('Server does not support TLS')
self._disconnect_with_error(StreamError.TLS,
'tls-not-supported')
return
self._start_tls()
def _start_stream(self):
self._log.info('Start stream')
self._stream_id = None
self._dispatcher.reset_parser()
header = get_stream_header(self._domain, self._lang, self.is_websocket)
self.send_nonza(header)
self.state = StreamState.WAIT_FOR_STREAM_START
def _start_tls(self):
self.send_nonza(TLSRequest())
self.state = StreamState.WAIT_FOR_TLS_PROCEED
def _start_auth(self, features):
if not features.has_sasl():
self._log.error('Server does not support SASL')
self._disconnect_with_error(StreamError.SASL,
'sasl-not-supported')
return
self.state = StreamState.PROCEED_WITH_AUTH
self._sasl.start_auth(features)
def _start_bind(self):
self._log.info('Send bind')
bind_request = BindRequest(self.resource)
self.send_stanza(bind_request)
self.state = StreamState.WAIT_FOR_BIND
def _on_bind(self, stanza):
if not isResultNode(stanza):
self._disconnect_with_error(StreamError.BIND,
stanza.getError(),
stanza.getErrorMsg())
return
jid = stanza.getTag('bind').getTagData('jid')
self._log.info('Successfully bound %s', jid)
self._set_bound_jid(jid)
if not self._session_required:
# Server don't want us to initialize a session
self._log.info('No session required')
self.set_state(StreamState.BIND_SUCCESSFUL)
else:
session_request = SessionRequest()
self.send_stanza(session_request)
self.state = StreamState.WAIT_FOR_SESSION
def _on_session(self, stanza):
if isResultNode(stanza):
self._log.info('Successfully started session')
self.set_state(StreamState.BIND_SUCCESSFUL)
else:
self._log.error('Session open failed')
self._disconnect_with_error(StreamError.SESSION,
stanza.getError(),
stanza.getErrorMsg())
def _ping(self):
self._ping_source_id = None
self._ping_task = self.get_module('Ping').ping(
self.domain,
timeout=10,
callback=self._on_pong)
def _on_pong(self, task):
self._ping_task = None
try:
task.finish()
except TimeoutStanzaError:
self._log.info('Ping timeout')
self._disconnect(immediate=True)
return
except CancelledError:
return
except StanzaError:
pass
self._log.info('Pong')
def _cancel_ping_task(self):
if self._ping_task is not None:
self._ping_task.cancel()
def register_handler(self, *args, **kwargs):
self._dispatcher.register_handler(*args, **kwargs)
def unregister_handler(self, *args, **kwargs):
self._dispatcher.unregister_handler(*args, **kwargs)
def destroy(self):
for task in self._tasks:
task.cancel()
self._remove_ping_timer()
self._smacks = None
self._sasl = None
self._dispatcher.cleanup()
self._dispatcher = None
self.remove_subscriptions()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/connection.py 0000664 0000000 0000000 00000010274 14130341156 0021661 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from gi.repository import Gio
from nbxmpp.const import TCPState
from nbxmpp.util import Observable
from nbxmpp.util import LogAdapter
log = logging.getLogger('nbxmpp.connection')
class Connection(Observable):
'''
Base Connection Class
Signals:
data-sent
data-received
bad-certificate
certificate-set
connection-failed
disconnected
'''
def __init__(self,
log_context,
address,
accepted_certificates,
ignore_tls_errors,
ignored_tls_errors,
client_cert):
self._log = LogAdapter(log, {'context': log_context})
Observable.__init__(self, self._log)
self._client_cert = client_cert
self._address = address
self._local_address = None
self._remote_address = None
self._state = None
self._state = TCPState.DISCONNECTED
self._peer_certificate = None
self._peer_certificate_errors = None
self._accepted_certificates = accepted_certificates
self._ignore_tls_errors = ignore_tls_errors
self._ignored_tls_errors = ignored_tls_errors
@property
def local_address(self):
return self._local_address
@property
def remote_address(self):
return self._remote_address
@property
def peer_certificate(self):
return (self._peer_certificate, self._peer_certificate_errors)
@property
def connection_type(self):
return self._address.type
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._log.info('Set Connection State: %s', value)
self._state = value
def _accept_certificate(self):
if not self._peer_certificate_errors:
return True
self._log.info('Found TLS certificate errors: %s',
self._peer_certificate_errors)
if self._ignore_tls_errors:
self._log.warning('Ignore all errors')
return True
if self._ignored_tls_errors:
self._log.warning('Ignore TLS certificate errors: %s',
self._ignored_tls_errors)
self._peer_certificate_errors -= self._ignored_tls_errors
if Gio.TlsCertificateFlags.UNKNOWN_CA in self._peer_certificate_errors:
for accepted_certificate in self._accepted_certificates:
if self._peer_certificate.is_same(accepted_certificate):
self._peer_certificate_errors.discard(
Gio.TlsCertificateFlags.UNKNOWN_CA)
break
if not self._peer_certificate_errors:
return True
return False
def disconnect(self):
raise NotImplementedError
def connect(self):
raise NotImplementedError
def send(self, stanza, now=False):
raise NotImplementedError
def _log_stanza(self, data, received=True):
direction = 'RECEIVED' if received else 'SENT'
message = ('::::: DATA %s ::::\n\n%s\n')
self._log.info(message, direction, data)
def start_tls_negotiation(self):
raise NotImplementedError
def shutdown_output(self):
raise NotImplementedError
def shutdown_input(self):
raise NotImplementedError
def destroy(self):
self.remove_subscriptions()
self._peer_certificate = None
self._client_cert = None
self._address = None
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/const.py 0000664 0000000 0000000 00000031675 14130341156 0020660 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from enum import Enum
from enum import IntEnum
from functools import total_ordering
from gi.repository import Gio
class IqType(Enum):
GET = 'get'
SET = 'set'
RESULT = 'result'
ERROR = 'error'
@property
def is_get(self):
return self == IqType.GET
@property
def is_set(self):
return self == IqType.SET
@property
def is_result(self):
return self == IqType.RESULT
@property
def is_error(self):
return self == IqType.ERROR
class MessageType(Enum):
NORMAL = 'normal'
CHAT = 'chat'
GROUPCHAT = 'groupchat'
HEADLINE = 'headline'
ERROR = 'error'
@property
def is_normal(self):
return self == MessageType.NORMAL
@property
def is_chat(self):
return self == MessageType.CHAT
@property
def is_groupchat(self):
return self == MessageType.GROUPCHAT
@property
def is_headline(self):
return self == MessageType.HEADLINE
@property
def is_error(self):
return self == MessageType.ERROR
class PresenceType(Enum):
PROBE = 'probe'
SUBSCRIBE = 'subscribe'
SUBSCRIBED = 'subscribed'
AVAILABLE = None
UNAVAILABLE = 'unavailable'
UNSUBSCRIBE = 'unsubscribe'
UNSUBSCRIBED = 'unsubscribed'
ERROR = 'error'
@property
def is_available(self):
return self == PresenceType.AVAILABLE
@property
def is_unavailable(self):
return self == PresenceType.UNAVAILABLE
@property
def is_error(self):
return self == PresenceType.ERROR
@property
def is_probe(self):
return self == PresenceType.PROBE
@property
def is_unsubscribe(self):
return self == PresenceType.UNSUBSCRIBE
@property
def is_unsubscribed(self):
return self == PresenceType.UNSUBSCRIBED
@property
def is_subscribe(self):
return self == PresenceType.SUBSCRIBE
@property
def is_subscribed(self):
return self == PresenceType.SUBSCRIBED
@total_ordering
class PresenceShow(Enum):
ONLINE = 'online'
CHAT = 'chat'
AWAY = 'away'
XA = 'xa'
DND = 'dnd'
@property
def is_online(self):
return self == PresenceShow.ONLINE
@property
def is_chat(self):
return self == PresenceShow.CHAT
@property
def is_away(self):
return self == PresenceShow.AWAY
@property
def is_xa(self):
return self == PresenceShow.XA
@property
def is_dnd(self):
return self == PresenceShow.DND
def __lt__(self, other):
try:
w1 = self._WEIGHTS[self]
w2 = self._WEIGHTS[other]
except KeyError:
return NotImplemented
return w1 < w2
PresenceShow._WEIGHTS = {
PresenceShow.CHAT: 1,
PresenceShow.ONLINE: 0,
PresenceShow.AWAY: -1,
PresenceShow.XA: -2,
PresenceShow.DND: -3,
}
class StatusCode(Enum):
NON_ANONYMOUS = '100'
AFFILIATION_CHANGE = '101'
SHOWING_UNAVAILABLE = '102'
NOT_SHOWING_UNAVAILABLE = '103'
CONFIG_NON_PRIVACY_RELATED = '104'
SELF = '110'
CONFIG_ROOM_LOGGING = '170'
CONFIG_NO_ROOM_LOGGING = '171'
CONFIG_NON_ANONYMOUS = '172'
CONFIG_SEMI_ANONYMOUS = '173'
CONFIG_FULL_ANONYMOUS = '174'
CREATED = '201'
NICKNAME_MODIFIED = '210'
REMOVED_BANNED = '301'
NICKNAME_CHANGE = '303'
REMOVED_KICKED = '307'
REMOVED_AFFILIATION_CHANGE = '321'
REMOVED_NONMEMBER_IN_MEMBERS_ONLY = '322'
REMOVED_SERVICE_SHUTDOWN = '332'
REMOVED_ERROR = '333'
class InviteType(Enum):
MEDIATED = 'mediated'
DIRECT = 'direct'
class AvatarState(Enum):
IGNORE = 'ignore'
NOT_READY = 'not ready'
EMPTY = 'empty'
ADVERTISED = 'advertised'
@total_ordering
class Affiliation(Enum):
OWNER = 'owner'
ADMIN = 'admin'
MEMBER = 'member'
OUTCAST = 'outcast'
NONE = 'none'
@property
def is_owner(self):
return self == Affiliation.OWNER
@property
def is_admin(self):
return self == Affiliation.ADMIN
@property
def is_member(self):
return self == Affiliation.MEMBER
@property
def is_outcast(self):
return self == Affiliation.OUTCAST
@property
def is_none(self):
return self == Affiliation.NONE
def __lt__(self, other):
try:
w1 = self._WEIGHTS[self]
w2 = self._WEIGHTS[other]
except KeyError:
return NotImplemented
return w1 < w2
Affiliation._WEIGHTS = {
Affiliation.OWNER: 4,
Affiliation.ADMIN: 3,
Affiliation.MEMBER: 2,
Affiliation.NONE: 1,
Affiliation.OUTCAST: 0,
}
@total_ordering
class Role(Enum):
MODERATOR = 'moderator'
PARTICIPANT = 'participant'
VISITOR = 'visitor'
NONE = 'none'
@property
def is_moderator(self):
return self == Role.MODERATOR
@property
def is_participant(self):
return self == Role.PARTICIPANT
@property
def is_visitor(self):
return self == Role.VISITOR
@property
def is_none(self):
return self == Role.NONE
def __lt__(self, other):
try:
w1 = self._WEIGHTS[self]
w2 = self._WEIGHTS[other]
except KeyError:
return NotImplemented
return w1 < w2
Role._WEIGHTS = {
Role.MODERATOR: 3,
Role.PARTICIPANT: 2,
Role.VISITOR: 1,
Role.NONE: 0,
}
class AnonymityMode(Enum):
UNKNOWN = None
SEMI = 'semi'
NONE = 'none'
class AdHocStatus(Enum):
EXECUTING = 'executing'
COMPLETED = 'completed'
CANCELED = 'canceled'
class AdHocAction(Enum):
EXECUTE = 'execute'
CANCEL = 'cancel'
PREV = 'prev'
NEXT = 'next'
COMPLETE = 'complete'
class AdHocNoteType(Enum):
INFO = 'info'
WARN = 'warn'
ERROR = 'error'
class ConnectionType(Enum):
DIRECT_TLS = 'DIRECT TLS'
START_TLS = 'START TLS'
PLAIN = 'PLAIN'
@property
def is_direct_tls(self):
return self == ConnectionType.DIRECT_TLS
@property
def is_start_tls(self):
return self == ConnectionType.START_TLS
@property
def is_plain(self):
return self == ConnectionType.PLAIN
class ConnectionProtocol(IntEnum):
TCP = 0
WEBSOCKET = 1
class StreamState(Enum):
RESOLVE = 'resolve'
RESOLVED = 'resolved'
CONNECTING = 'connecting'
CONNECTED = 'connected'
DISCONNECTED = 'disconnected'
DISCONNECTING = 'disconnecting'
STREAM_START = 'stream start'
WAIT_FOR_STREAM_START = 'wait for stream start'
WAIT_FOR_FEATURES = 'wait for features'
WAIT_FOR_TLS_PROCEED = 'wait for tls proceed'
TLS_START_SUCCESSFUL = 'tls start successful'
PROCEED_WITH_AUTH = 'proceed with auth'
AUTH_SUCCESSFUL = 'auth successful'
AUTH_FAILED = 'auth failed'
WAIT_FOR_RESUMED = 'wait for resumed'
RESUME_FAILED = 'resume failed'
RESUME_SUCCESSFUL = 'resume successful'
PROCEED_WITH_BIND = 'proceed with bind'
BIND_SUCCESSFUL = 'bind successful'
WAIT_FOR_BIND = 'wait for bind'
WAIT_FOR_SESSION = 'wait for session'
ACTIVE = 'active'
class StreamError(Enum):
PARSING = 0
CONNECTION_FAILED = 1
SESSION = 2
BIND = 3
TLS = 4
BAD_CERTIFICATE = 5
STREAM = 6
SASL = 7
REGISTER = 8
END = 9
class TCPState(Enum):
DISCONNECTED = 'disconnected'
DISCONNECTING = 'disconnecting'
CONNECTING = 'connecting'
CONNECTED = 'connected'
class Mode(IntEnum):
CLIENT = 0
REGISTER = 1
LOGIN_TEST = 2
ANONYMOUS_TEST = 3
@property
def is_client(self):
return self == Mode.CLIENT
@property
def is_register(self):
return self == Mode.REGISTER
@property
def is_login_test(self):
return self == Mode.LOGIN_TEST
@property
def is_anonymous_test(self):
return self == Mode.ANONYMOUS_TEST
MOODS = [
'afraid',
'amazed',
'amorous',
'angry',
'annoyed',
'anxious',
'aroused',
'ashamed',
'bored',
'brave',
'calm',
'cautious',
'cold',
'confident',
'confused',
'contemplative',
'contented',
'cranky',
'crazy',
'creative',
'curious',
'dejected',
'depressed',
'disappointed',
'disgusted',
'dismayed',
'distracted',
'embarrassed',
'envious',
'excited',
'flirtatious',
'frustrated',
'grateful',
'grieving',
'grumpy',
'guilty',
'happy',
'hopeful',
'hot',
'humbled',
'humiliated',
'hungry',
'hurt',
'impressed',
'in_awe',
'in_love',
'indignant',
'interested',
'intoxicated',
'invincible',
'jealous',
'lonely',
'lost',
'lucky',
'mean',
'moody',
'nervous',
'neutral',
'offended',
'outraged',
'playful',
'proud',
'relaxed',
'relieved',
'remorseful',
'restless',
'sad',
'sarcastic',
'satisfied',
'serious',
'shocked',
'shy',
'sick',
'sleepy',
'spontaneous',
'stressed',
'strong',
'surprised',
'thankful',
'thirsty',
'tired',
'undefined',
'weak',
'worried']
ACTIVITIES = {
'doing_chores': [
'buying_groceries',
'cleaning',
'cooking',
'doing_maintenance',
'doing_the_dishes',
'doing_the_laundry',
'gardening',
'running_an_errand',
'walking_the_dog'],
'drinking': [
'having_a_beer',
'having_coffee',
'having_tea'],
'eating': [
'having_a_snack',
'having_breakfast',
'having_dinner',
'having_lunch'],
'exercising': [
'cycling',
'dancing',
'hiking',
'jogging',
'playing_sports',
'running',
'skiing',
'swimming',
'working_out'],
'grooming': [
'at_the_spa',
'brushing_teeth',
'getting_a_haircut',
'shaving',
'taking_a_bath',
'taking_a_shower'],
'having_appointment': [],
'inactive': [
'day_off',
'hanging_out',
'hiding',
'on_vacation',
'praying',
'scheduled_holiday',
'sleeping',
'thinking'],
'relaxing': [
'fishing',
'gaming',
'going_out',
'partying',
'reading',
'rehearsing',
'shopping',
'smoking',
'socializing',
'sunbathing',
'watching_tv',
'watching_a_movie'],
'talking': [
'in_real_life',
'on_the_phone',
'on_video_phone'],
'traveling': [
'commuting',
'cycling',
'driving',
'in_a_car',
'on_a_bus',
'on_a_plane',
'on_a_train',
'on_a_trip',
'walking'],
'working': [
'coding',
'in_a_meeting',
'studying',
'writing']
}
LOCATION_DATA = [
'accuracy',
'alt',
'altaccuracy',
'area',
'bearing',
'building',
'country',
'countrycode',
'datum',
'description',
'error',
'floor',
'lat',
'locality',
'lon',
'postalcode',
'region',
'room',
'speed',
'street',
'text',
'timestamp',
'tzo',
'uri']
TUNE_DATA = [
'artist',
'length',
'rating',
'source',
'title',
'track',
'uri']
CHATSTATES = [
'active',
'inactive',
'gone',
'composing',
'paused'
]
REGISTER_FIELDS = [
'username',
'nick',
'password',
'name',
'first',
'last',
'email',
'address',
'city',
'state',
'zip',
'phone',
'url',
'date',
]
# pylint: disable=line-too-long
GIO_TLS_ERRORS = {
Gio.TlsCertificateFlags.UNKNOWN_CA: 'The signing certificate authority is not known',
Gio.TlsCertificateFlags.REVOKED: 'The certificate has been revoked',
Gio.TlsCertificateFlags.BAD_IDENTITY: 'The certificate does not match the expected identity of the site',
Gio.TlsCertificateFlags.INSECURE: 'The certificate’s algorithm is insecure',
Gio.TlsCertificateFlags.NOT_ACTIVATED: 'The certificate’s activation time is in the future',
Gio.TlsCertificateFlags.GENERIC_ERROR: 'Unknown validation error',
Gio.TlsCertificateFlags.EXPIRED: 'The certificate has expired',
}
# pylint: enable=line-too-long
NOT_ALLOWED_XML_CHARS = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
'\x0C': '',
'\x1B': ''
}
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/dispatcher.py 0000664 0000000 0000000 00000045214 14130341156 0021652 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
import re
import time
from xml.parsers.expat import ExpatError
from gi.repository import GLib
from nbxmpp.simplexml import NodeBuilder
from nbxmpp.simplexml import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import InvalidFrom
from nbxmpp.protocol import InvalidJid
from nbxmpp.protocol import InvalidStanza
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Presence
from nbxmpp.protocol import Message
from nbxmpp.protocol import Protocol
from nbxmpp.protocol import Error
from nbxmpp.protocol import StreamErrorNode
from nbxmpp.protocol import ERR_FEATURE_NOT_IMPLEMENTED
from nbxmpp.modules.eme import EME
from nbxmpp.modules.http_auth import HTTPAuth
from nbxmpp.modules.presence import BasePresence
from nbxmpp.modules.message import BaseMessage
from nbxmpp.modules.iq import BaseIq
from nbxmpp.modules.nickname import Nickname
from nbxmpp.modules.delay import Delay
from nbxmpp.modules.muc import MUC
from nbxmpp.modules.idle import Idle
from nbxmpp.modules.pgplegacy import PGPLegacy
from nbxmpp.modules.vcard_avatar import VCardAvatar
from nbxmpp.modules.captcha import Captcha
from nbxmpp.modules.entity_caps import EntityCaps
from nbxmpp.modules.blocking import Blocking
from nbxmpp.modules.pubsub import PubSub
from nbxmpp.modules.activity import Activity
from nbxmpp.modules.tune import Tune
from nbxmpp.modules.mood import Mood
from nbxmpp.modules.location import Location
from nbxmpp.modules.user_avatar import UserAvatar
from nbxmpp.modules.bookmarks.private_bookmarks import PrivateBookmarks
from nbxmpp.modules.bookmarks.pep_bookmarks import PEPBookmarks
from nbxmpp.modules.bookmarks.native_bookmarks import NativeBookmarks
from nbxmpp.modules.openpgp import OpenPGP
from nbxmpp.modules.omemo import OMEMO
from nbxmpp.modules.annotations import Annotations
from nbxmpp.modules.muclumbus import Muclumbus
from nbxmpp.modules.software_version import SoftwareVersion
from nbxmpp.modules.adhoc import AdHoc
from nbxmpp.modules.ibb import IBB
from nbxmpp.modules.discovery import Discovery
from nbxmpp.modules.chat_markers import ChatMarkers
from nbxmpp.modules.receipts import Receipts
from nbxmpp.modules.oob import OOB
from nbxmpp.modules.correction import Correction
from nbxmpp.modules.attention import Attention
from nbxmpp.modules.security_labels import SecurityLabels
from nbxmpp.modules.chatstates import Chatstates
from nbxmpp.modules.register import Register
from nbxmpp.modules.http_upload import HTTPUpload
from nbxmpp.modules.mam import MAM
from nbxmpp.modules.vcard_temp import VCardTemp
from nbxmpp.modules.vcard4 import VCard4
from nbxmpp.modules.ping import Ping
from nbxmpp.modules.delimiter import Delimiter
from nbxmpp.modules.misc import unwrap_carbon
from nbxmpp.modules.misc import unwrap_mam
from nbxmpp.util import get_properties_struct
from nbxmpp.util import get_invalid_xml_regex
from nbxmpp.util import is_websocket_close
from nbxmpp.util import is_websocket_stream_error
from nbxmpp.util import Observable
from nbxmpp.util import LogAdapter
log = logging.getLogger('nbxmpp.dispatcher')
class StanzaDispatcher(Observable):
"""
Dispatches stanzas to handlers
Signals:
before-dispatch
parsing-error
stream-end
"""
def __init__(self, client):
Observable.__init__(self, log)
self._client = client
self._modules = {}
self._parser = None
self._websocket_stream_error = None
self._log = LogAdapter(log, {'context': client.log_context})
self._handlers = {}
self._id_callbacks = {}
self._dispatch_callback = None
self._timeout_id = None
self._stanza_types = {
'iq': Iq,
'message': Message,
'presence': Presence,
'error': StreamErrorNode,
}
self.invalid_chars_re = get_invalid_xml_regex()
self._register_namespace('unknown')
self._register_namespace(Namespace.STREAMS)
self._register_namespace(Namespace.CLIENT)
self._register_protocol('iq', Iq)
self._register_protocol('presence', Presence)
self._register_protocol('message', Message)
self._register_modules()
def set_dispatch_callback(self, callback):
self._log.info('Set dispatch callback: %s', callback)
self._dispatch_callback = callback
def get_module(self, name):
return self._modules[name]
def _register_modules(self):
self._modules['BasePresence'] = BasePresence(self._client)
self._modules['BaseMessage'] = BaseMessage(self._client)
self._modules['BaseIq'] = BaseIq(self._client)
self._modules['EME'] = EME(self._client)
self._modules['HTTPAuth'] = HTTPAuth(self._client)
self._modules['Nickname'] = Nickname(self._client)
self._modules['MUC'] = MUC(self._client)
self._modules['Delay'] = Delay(self._client)
self._modules['Captcha'] = Captcha(self._client)
self._modules['Idle'] = Idle(self._client)
self._modules['PGPLegacy'] = PGPLegacy(self._client)
self._modules['VCardAvatar'] = VCardAvatar(self._client)
self._modules['EntityCaps'] = EntityCaps(self._client)
self._modules['Blocking'] = Blocking(self._client)
self._modules['PubSub'] = PubSub(self._client)
self._modules['Mood'] = Mood(self._client)
self._modules['Activity'] = Activity(self._client)
self._modules['Tune'] = Tune(self._client)
self._modules['Location'] = Location(self._client)
self._modules['UserAvatar'] = UserAvatar(self._client)
self._modules['PrivateBookmarks'] = PrivateBookmarks(self._client)
self._modules['PEPBookmarks'] = PEPBookmarks(self._client)
self._modules['NativeBookmarks'] = NativeBookmarks(self._client)
self._modules['OpenPGP'] = OpenPGP(self._client)
self._modules['OMEMO'] = OMEMO(self._client)
self._modules['Annotations'] = Annotations(self._client)
self._modules['Muclumbus'] = Muclumbus(self._client)
self._modules['SoftwareVersion'] = SoftwareVersion(self._client)
self._modules['AdHoc'] = AdHoc(self._client)
self._modules['IBB'] = IBB(self._client)
self._modules['Discovery'] = Discovery(self._client)
self._modules['ChatMarkers'] = ChatMarkers(self._client)
self._modules['Receipts'] = Receipts(self._client)
self._modules['OOB'] = OOB(self._client)
self._modules['Correction'] = Correction(self._client)
self._modules['Attention'] = Attention(self._client)
self._modules['SecurityLabels'] = SecurityLabels(self._client)
self._modules['Chatstates'] = Chatstates(self._client)
self._modules['Register'] = Register(self._client)
self._modules['HTTPUpload'] = HTTPUpload(self._client)
self._modules['MAM'] = MAM(self._client)
self._modules['VCardTemp'] = VCardTemp(self._client)
self._modules['VCard4'] = VCard4(self._client)
self._modules['Ping'] = Ping(self._client)
self._modules['Delimiter'] = Delimiter(self._client)
for instance in self._modules.values():
for handler in instance.handlers:
self.register_handler(handler)
def reset_parser(self):
if self._parser is not None:
self._parser.dispatch = None
self._parser.destroy()
self._parser = None
self._parser = NodeBuilder(dispatch_depth=2,
finished=False)
self._parser.dispatch = self.dispatch
def replace_non_character(self, data):
return re.sub(self.invalid_chars_re, '\ufffd', data)
def process_data(self, data):
# Parse incoming data
data = self.replace_non_character(data)
if self._client.is_websocket:
stanza = Node(node=data)
if is_websocket_stream_error(stanza):
for tag in stanza.getChildren():
name = tag.getName()
if (name != 'text' and
tag.getNamespace() == Namespace.XMPP_STREAMS):
self._websocket_stream_error = name
elif is_websocket_close(stanza):
self._log.info('Stream received')
self.notify('stream-end', self._websocket_stream_error)
return
self.dispatch(stanza)
return
try:
self._parser.Parse(data)
except (ExpatError, ValueError) as error:
self._log.error('XML parsing error: %s', error)
self.notify('parsing-error', error)
return
# end stream:stream tag received
if self._parser.has_received_endtag():
self._log.info('End of stream: %s', self._parser.streamError)
self.notify('stream-end', self._parser.streamError)
return
def _register_namespace(self, xmlns):
"""
Setup handler structure for namespace
"""
self._log.debug('Register namespace "%s"', xmlns)
self._handlers[xmlns] = {}
self._register_protocol('error', Protocol, xmlns=xmlns)
self._register_protocol('unknown', Protocol, xmlns=xmlns)
self._register_protocol('default', Protocol, xmlns=xmlns)
def _register_protocol(self, tag_name, protocol, xmlns=None):
"""
Register protocol for top level tag names
"""
if xmlns is None:
xmlns = Namespace.CLIENT
self._log.debug('Register protocol "%s (%s)" as %s',
tag_name, xmlns, protocol)
self._handlers[xmlns][tag_name] = {'type': protocol, 'default': []}
def register_handler(self, handler):
"""
Register handler
"""
xmlns = handler.xmlns or Namespace.CLIENT
typ = handler.typ
if not typ and not handler.ns:
typ = 'default'
self._log.debug(
'Register handler %s for "%s" type->%s ns->%s(%s) priority->%s',
handler.callback, handler.name, typ, handler.ns,
xmlns, handler.priority
)
if xmlns not in self._handlers:
self._register_namespace(xmlns)
if handler.name not in self._handlers[xmlns]:
self._register_protocol(handler.name, Protocol, xmlns)
specific = typ + handler.ns
if specific not in self._handlers[xmlns][handler.name]:
self._handlers[xmlns][handler.name][specific] = []
self._handlers[xmlns][handler.name][specific].append(
{'func': handler.callback,
'priority': handler.priority,
'specific': specific})
def unregister_handler(self, handler):
"""
Unregister handler
"""
xmlns = handler.xmlns or Namespace.CLIENT
typ = handler.typ
if not typ and not handler.ns:
typ = 'default'
specific = typ + handler.ns
try:
self._handlers[xmlns][handler.name][specific]
except KeyError:
return
for handler_dict in self._handlers[xmlns][handler.name][specific]:
if handler_dict['func'] != handler.callback:
continue
try:
self._handlers[xmlns][handler.name][specific].remove(
handler_dict)
except ValueError:
self._log.warning(
'Unregister failed: %s for "%s" type->%s ns->%s(%s)',
handler.callback, handler.name, typ, handler.ns, xmlns)
else:
self._log.debug(
'Unregister handler %s for "%s" type->%s ns->%s(%s)',
handler.callback, handler.name, typ, handler.ns, xmlns)
def _default_handler(self, stanza):
"""
Return stanza back to the sender with error
"""
if stanza.getType() in ('get', 'set'):
self._client.send_stanza(Error(stanza, ERR_FEATURE_NOT_IMPLEMENTED))
def dispatch(self, stanza):
self.notify('before-dispatch', stanza)
if self._dispatch_callback is not None:
name = stanza.getName()
protocol_class = self._stanza_types.get(name)
if protocol_class is not None:
stanza = protocol_class(node=stanza)
self._dispatch_callback(stanza)
return
# Count stanza
self._client._smacks.count_incoming(stanza.getName())
name = stanza.getName()
xmlns = stanza.getNamespace()
if xmlns not in self._handlers:
self._log.warning('Unknown namespace: %s', xmlns)
xmlns = 'unknown'
if name not in self._handlers[xmlns]:
self._log.warning('Unknown stanza: %s', stanza)
name = 'unknown'
# Convert simplexml to Protocol object
try:
stanza = self._handlers[xmlns][name]['type'](node=stanza)
except InvalidJid:
self._log.warning('Invalid JID, ignoring stanza')
self._log.warning(stanza)
return
own_jid = self._client.get_bound_jid()
properties = get_properties_struct(name, own_jid)
if name == 'iq':
if stanza.getFrom() is None and own_jid is not None:
stanza.setFrom(own_jid.bare)
if name == 'message':
# https://tools.ietf.org/html/rfc6120#section-8.1.1.1
# If the stanza does not include a 'to' address then the client MUST
# treat it as if the 'to' address were included with a value of the
# client's full JID.
to = stanza.getTo()
if to is None:
stanza.setTo(own_jid)
elif not to.bare_match(own_jid):
self._log.warning('Message addressed to someone else: %s',
stanza)
return
if stanza.getFrom() is None:
stanza.setFrom(own_jid.bare)
# Unwrap carbon
try:
stanza, properties.carbon = unwrap_carbon(stanza, own_jid)
except (InvalidFrom, InvalidJid) as exc:
self._log.warning(exc)
self._log.warning(stanza)
return
except NodeProcessed as exc:
self._log.info(exc)
return
# Unwrap mam
try:
stanza, properties.mam = unwrap_mam(stanza, own_jid)
except (InvalidStanza, InvalidJid) as exc:
self._log.warning(exc)
self._log.warning(stanza)
return
typ = stanza.getType()
if name == 'message' and not typ:
typ = 'normal'
elif not typ:
typ = ''
stanza.props = stanza.getProperties()
self._log.debug('type: %s, properties: %s', typ, stanza.props)
# Process callbacks
_id = stanza.getID()
func, _timeout, user_data = self._id_callbacks.pop(
_id, (None, None, {}))
if user_data is None:
user_data = {}
if func is not None:
try:
func(self._client, stanza, **user_data)
except Exception:
self._log.exception('Error while handling stanza')
return
# Gather specifics depending on stanza properties
specifics = ['default']
if typ and typ in self._handlers[xmlns][name]:
specifics.append(typ)
for prop in stanza.props:
if prop in self._handlers[xmlns][name]:
specifics.append(prop)
if typ and typ + prop in self._handlers[xmlns][name]:
specifics.append(typ + prop)
# Create the handler chain
chain = []
chain += self._handlers[xmlns]['default']['default']
for specific in specifics:
chain += self._handlers[xmlns][name][specific]
# Sort chain with priority
chain.sort(key=lambda x: x['priority'])
for handler in chain:
self._log.info('Call handler: %s', handler['func'].__qualname__)
try:
handler['func'](self._client, stanza, properties)
except NodeProcessed:
return
except Exception:
self._log.exception('Handler exception:')
return
# Stanza was not processed call default handler
self._default_handler(stanza)
def add_callback_for_id(self, id_, func, timeout, user_data):
if timeout is not None and self._timeout_id is None:
self._log.info('Add timeout check')
self._timeout_id = GLib.timeout_add_seconds(
1, self._timeout_check)
timeout = time.monotonic() + timeout
self._id_callbacks[id_] = (func, timeout, user_data)
def _timeout_check(self):
self._log.info('Run timeout check')
timeouts = {}
for id_, data in self._id_callbacks.items():
if data[1] is not None:
timeouts[id_] = data
if not timeouts:
self._log.info('Remove timeout check, no timeouts scheduled')
self._timeout_id = None
return False
for id_, data in timeouts.items():
func, timeout, user_data = data
if user_data is None:
user_data = {}
if timeout < time.monotonic():
self._id_callbacks.pop(id_)
func(self._client, None, **user_data)
return True
def _remove_timeout_source(self):
if self._timeout_id is not None:
GLib.source_remove(self._timeout_id)
self._timeout_id = None
def remove_iq_callback(self, id_):
self._id_callbacks.pop(id_, None)
def clear_iq_callbacks(self):
self._log.info('Clear IQ callbacks')
self._id_callbacks.clear()
def cleanup(self):
self._client = None
self._modules = {}
self._parser = None
self.clear_iq_callbacks()
self._dispatch_callback = None
self._handlers.clear()
self._remove_timeout_source()
self.remove_subscriptions()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/errors.py 0000664 0000000 0000000 00000011122 14130341156 0021027 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from nbxmpp.namespaces import Namespace
def is_error(error):
return isinstance(error, BaseError)
class BaseError(Exception):
def __init__(self, is_fatal=False):
self.is_fatal = is_fatal
self.text = ''
def __str__(self):
return self.text
def get_text(self):
return self.text
class StanzaError(BaseError):
log_level = logging.INFO
app_namespace = None
def __init__(self, stanza):
BaseError.__init__(self)
self.stanza = stanza
self._stanza_name = stanza.getName()
self._error_node = stanza.getTag('error')
self.condition = stanza.getError()
self.condition_data = self._error_node.getTagData(self.condition)
self.app_condition = self._get_app_condition()
self.type = stanza.getErrorType()
self.jid = stanza.getFrom()
self.id = stanza.getID()
self._text = {}
text_elements = self._error_node.getTags('text',
namespace=Namespace.STANZAS)
for element in text_elements:
lang = element.getXmlLang()
text = element.getData()
self._text[lang] = text
def _get_app_condition(self):
if self.app_namespace is None:
return None
for node in self._error_node.getChildren():
if node.getNamespace() == self.app_namespace:
return node.getName()
return None
def get_text(self, pref_lang=None):
if pref_lang is not None:
text = self._text.get(pref_lang)
if text is not None:
return text
if self._text:
text = self._text.get('en')
if text is not None:
return text
text = self._text.get(None)
if text is not None:
return text
return self._text.popitem()[1]
return ''
def set_text(self, lang, text):
self._text[lang] = text
def __str__(self):
condition = self.condition
if self.app_condition is not None:
condition = '%s (%s)' % (self.condition, self.app_condition)
text = self.get_text('en') or ''
if text:
text = ' - %s' % text
return 'Error from %s: %s%s' % (self.jid, condition, text)
class PubSubStanzaError(StanzaError):
app_namespace = Namespace.PUBSUB_ERROR
class HTTPUploadStanzaError(StanzaError):
app_namespace = Namespace.HTTPUPLOAD_0
def get_max_file_size(self):
if self.app_condition != 'file-too-large':
return None
node = self._error_node.getTag(self.app_condition)
try:
return float(node.getTagData('max-file-size'))
except Exception:
return None
def get_retry_date(self):
if self.app_condition != 'retry':
return None
return self._error_node.getTagAttr('stamp')
class MalformedStanzaError(BaseError):
log_level = logging.WARNING
def __init__(self, text, stanza, is_fatal=True):
BaseError.__init__(self, is_fatal=is_fatal)
self.stanza = stanza
self.text = str(text)
class CancelledError(BaseError):
log_level = logging.INFO
def __init__(self):
BaseError.__init__(self, is_fatal=True)
self.text = 'Task has been cancelled'
class TimeoutStanzaError(BaseError):
log_level = logging.INFO
def __init__(self):
BaseError.__init__(self)
self.text = 'Timeout reached'
class RegisterStanzaError(StanzaError):
def __init__(self, stanza, data):
StanzaError.__init__(self, stanza)
self._data = data
def get_data(self):
return self._data
class ChangePasswordStanzaError(StanzaError):
def __init__(self, stanza, form):
StanzaError.__init__(self, stanza)
self._form = form
def get_form(self):
return self._form
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/examples/ 0000775 0000000 0000000 00000000000 14130341156 0020762 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/examples/client.py 0000664 0000000 0000000 00000023476 14130341156 0022626 0 ustar 00root root 0000000 0000000 #!/usr/bin/python3
import os
import logging
import json
from pathlib import Path
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0')
from gi.repository import Gtk
from gi.repository import GLib
import nbxmpp
from nbxmpp.protocol import JID
from nbxmpp.client import Client
from nbxmpp.structs import ProxyData
from nbxmpp.structs import StanzaHandler
from nbxmpp.addresses import ServerAddress
from nbxmpp.const import ConnectionType
from nbxmpp.const import ConnectionProtocol
from nbxmpp.const import StreamError
consoleloghandler = logging.StreamHandler()
log = logging.getLogger('nbxmpp')
log.setLevel('INFO')
log.addHandler(consoleloghandler)
formatter = logging.Formatter(
'%(asctime)s %(levelname)-7s %(name)-25s %(message)s',
datefmt='%H:%M:%S')
consoleloghandler.setFormatter(formatter)
class Builder:
def __init__(self, filename):
file_path = Path(__file__).resolve()
ui_file_path = file_path.parent / filename
self._builder = Gtk.Builder()
self._builder.add_from_file(str(ui_file_path))
def __getattr__(self, name):
try:
return getattr(self._builder, name)
except AttributeError:
return self._builder.get_object(name)
class StanzaRow(Gtk.ListBoxRow):
def __init__(self, stanza, incoming):
Gtk.ListBoxRow.__init__(self)
color = 'red' if incoming else 'blue'
if isinstance(stanza, bytes):
stanza = str(stanza)
if not isinstance(stanza, str):
stanza = stanza.__str__(fancy=True)
stanza = GLib.markup_escape_text(stanza)
label = Gtk.Label()
label.set_markup('%s' % (color, stanza))
label.set_xalign(0)
label.set_halign(Gtk.Align.START)
self.add(label)
self.show_all()
class TestClient(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title='Test Client')
self.set_default_size(500, 500)
self._builder = Builder('client.ui')
self._builder.connect_signals(self)
self.add(self._builder.grid)
self._client = None
self._scroll_timeout = None
self._create_paths()
self._load_config()
def _create_client(self):
self._client = Client(log_context='TEST')
self._client.set_domain(self.address.domain)
self._client.set_username(self.address.localpart)
self._client.set_resource('test')
proxy_ip = self._builder.proxy_ip.get_text()
if proxy_ip:
proxy_port = int(self._builder.proxy_port.get_text())
proxy_host = '%s:%s' % (proxy_ip, proxy_port)
proxy = ProxyData(self._builder.proxy_type.get_active_text().lower(),
proxy_host,
self._builder.proxy_username.get_text() or None,
self._builder.proxy_password.get_text() or None)
self._client.set_proxy(proxy)
self._client.set_connection_types(self._get_connection_types())
self._client.set_protocols(self._get_connection_protocols())
self._client.set_password(self.password)
self._client.subscribe('resume-failed', self._on_signal)
self._client.subscribe('resume-successful', self._on_signal)
self._client.subscribe('disconnected', self._on_signal)
self._client.subscribe('connection-lost', self._on_signal)
self._client.subscribe('connection-failed', self._on_signal)
self._client.subscribe('connected', self._on_connected)
self._client.subscribe('stanza-sent', self._on_stanza_sent)
self._client.subscribe('stanza-received', self._on_stanza_received)
self._client.register_handler(StanzaHandler('message', self._on_message))
@property
def password(self):
return self._builder.password.get_text()
@property
def address(self):
return JID.from_string(self._builder.address.get_text())
@property
def xml_box(self):
return self._builder.xml_box
def scroll_to_end(self):
adj_v = self._builder.scrolledwin.get_vadjustment()
if adj_v is None:
# This can happen when the Widget is already destroyed when called
# from GLib.idle_add
self._scroll_timeout = None
return
max_scroll_pos = adj_v.get_upper() - adj_v.get_page_size()
adj_v.set_value(max_scroll_pos)
adj_h = self._builder.scrolledwin.get_hadjustment()
adj_h.set_value(0)
self._scroll_timeout = None
def _on_signal(self, _client, signal_name, *args, **kwargs):
log.info('%s, Error: %s', signal_name, self._client.get_error())
if signal_name == 'disconnected':
if self._client.get_error() is None:
return
domain, error, text = self._client.get_error()
if domain == StreamError.BAD_CERTIFICATE:
self._client.set_ignore_tls_errors(True)
self._client.connect()
def _on_connected(self, _client, _signal_name):
self.send_presence()
def _on_message(self, _stream, stanza, _properties):
log.info('Message received')
log.info(stanza.getBody())
def _on_stanza_sent(self, _stream, _signal_name, data):
self.xml_box.add(StanzaRow(data, False))
self._add_scroll_timeout()
def _on_stanza_received(self, _stream, _signal_name, data):
self.xml_box.add(StanzaRow(data, True))
self._add_scroll_timeout()
def _add_scroll_timeout(self):
if self._scroll_timeout is not None:
return
self._scroll_timeout = GLib.timeout_add(50, self.scroll_to_end)
def _connect_clicked(self, *args):
if self._client is None:
self._create_client()
self._client.connect()
def _disconnect_clicked(self, *args):
if self._client is not None:
self._client.disconnect()
def _clear_clicked(self, *args):
self.xml_box.foreach(self._remove)
def _on_reconnect_clicked(self, *args):
if self._client is not None:
self._client.reconnect()
def _get_connection_types(self):
types = []
if self._builder.directtls.get_active():
types.append(ConnectionType.DIRECT_TLS)
if self._builder.starttls.get_active():
types.append(ConnectionType.START_TLS)
if self._builder.plain.get_active():
types.append(ConnectionType.PLAIN)
return types
def _get_connection_protocols(self):
protocols = []
if self._builder.tcp.get_active():
protocols.append(ConnectionProtocol.TCP)
if self._builder.websocket.get_active():
protocols.append(ConnectionProtocol.WEBSOCKET)
return protocols
def _on_save_clicked(self, *args):
data = {}
data['jid'] = self._builder.address.get_text()
data['password'] = self._builder.password.get_text()
data['proxy_type'] = self._builder.proxy_type.get_active_text()
data['proxy_ip'] = self._builder.proxy_ip.get_text()
data['proxy_port'] = self._builder.proxy_port.get_text()
data['proxy_username'] = self._builder.proxy_username.get_text()
data['proxy_password'] = self._builder.proxy_password.get_text()
data['directtls'] = self._builder.directtls.get_active()
data['starttls'] = self._builder.starttls.get_active()
data['plain'] = self._builder.plain.get_active()
data['tcp'] = self._builder.tcp.get_active()
data['websocket'] = self._builder.websocket.get_active()
path = self._get_config_dir() / 'config'
with path.open('w') as fp:
json.dump(data, fp)
def _load_config(self):
path = self._get_config_dir() / 'config'
if not path.exists():
return
with path.open('r') as fp:
data = json.load(fp)
self._builder.address.set_text(data.get('jid', ''))
self._builder.password.set_text(data.get('password', ''))
self._builder.proxy_type.set_active_id(data.get('proxy_type', 'HTTP'))
self._builder.proxy_ip.set_text(data.get('proxy_ip', ''))
self._builder.proxy_port.set_text(data.get('proxy_port', ''))
self._builder.proxy_username.set_text(data.get('proxy_username', ''))
self._builder.proxy_password.set_text(data.get('proxy_password', ''))
self._builder.directtls.set_active(data.get('directtls', False))
self._builder.starttls.set_active(data.get('starttls', False))
self._builder.plain.set_active(data.get('plain', False))
self._builder.tcp.set_active(data.get('tcp', False))
self._builder.websocket.set_active(data.get('websocket', False))
@staticmethod
def _get_config_dir():
if os.name == 'nt':
return Path(os.path.join(os.environ['appdata'], 'nbxmpp'))
expand = os.path.expanduser
base = os.getenv('XDG_CONFIG_HOME')
if base is None or base[0] != '/':
base = expand('~/.config')
return Path(os.path.join(base, 'nbxmpp'))
def _create_paths(self):
path_ = self._get_config_dir()
if not path_.exists():
for parent_path in reversed(path_.parents):
# Create all parent folders
# don't use mkdir(parent=True), as it ignores `mode`
# when creating the parents
if not parent_path.exists():
print('creating %s directory' % parent_path)
parent_path.mkdir(mode=0o700)
print('creating %s directory' % path_)
path_.mkdir(mode=0o700)
def _remove(self, item):
self.xml_box.remove(item)
item.destroy()
def send_presence(self):
presence = nbxmpp.Presence()
self._client.send_stanza(presence)
win = TestClient()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/examples/client.ui 0000664 0000000 0000000 00000046230 14130341156 0022604 0 ustar 00root root 0000000 0000000
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/exceptions.py 0000664 0000000 0000000 00000001464 14130341156 0021704 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
class EndOfConnection(Exception):
pass
class NonFatalSSLError(Exception):
pass
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/idlequeue.py 0000664 0000000 0000000 00000044031 14130341156 0021502 0 ustar 00root root 0000000 0000000 ## idlequeue.py
##
## Copyright (C) 2006 Dimitur Kirov
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
"""
Idlequeues are Gajim's network heartbeat. Transports can be plugged as idle
objects and be informed about possible IO
"""
import os
import errno
import select
import logging
import time
import subprocess
# needed for get_idleqeue
try:
from gi.repository import GLib
HAVE_GLIB = True
except ImportError:
HAVE_GLIB = False
if os.name == 'posix':
import fcntl
log = logging.getLogger('nbxmpp.idlequeue')
if HAVE_GLIB:
FLAG_WRITE = GLib.IOCondition.OUT | GLib.IOCondition.HUP
FLAG_READ = GLib.IOCondition.IN | GLib.IOCondition.PRI | \
GLib.IOCondition.HUP
FLAG_READ_WRITE = GLib.IOCondition.OUT | GLib.IOCondition.IN | \
GLib.IOCondition.PRI | GLib.IOCondition.HUP
FLAG_CLOSE = GLib.IOCondition.HUP
PENDING_READ = GLib.IOCondition.IN # There is data to read.
PENDING_WRITE = GLib.IOCondition.OUT # Data CAN be written without blocking
IS_CLOSED = GLib.IOCondition.HUP # Hung up (connection broken)
else:
FLAG_WRITE = 20 # write only 10100
FLAG_READ = 19 # read only 10011
FLAG_READ_WRITE = 23 # read and write 10111
FLAG_CLOSE = 16 # wait for close 10000
PENDING_READ = 3 # waiting read event 11
PENDING_WRITE = 4 # waiting write event 100
IS_CLOSED = 16 # channel closed 10000
def get_idlequeue():
"""
Get an appropriate idlequeue
"""
if os.name == 'nt':
# gobject.io_add_watch does not work on windows
return SelectIdleQueue()
if HAVE_GLIB:
# Gajim's default Idlequeue
return GlibIdleQueue()
# GUI less implementation
return SelectIdleQueue()
class IdleObject:
"""
Idle listener interface. Listed methods are called by IdleQueue.
"""
def __init__(self):
self.fd = -1 #: filedescriptor, must be unique for each IdleObject
def pollend(self):
"""
Called on stream failure
"""
def pollin(self):
"""
Called on new read event
"""
def pollout(self):
"""
Called on new write event (connect in sockets is a pollout)
"""
def read_timeout(self):
"""
Called when timeout happened
"""
class IdleCommand(IdleObject):
"""
Can be subclassed to execute commands asynchronously by the idlequeue.
Result will be optained via file descriptor of created pipe
"""
def __init__(self, on_result):
IdleObject.__init__(self)
# how long (sec.) to wait for result ( 0 - forever )
# it is a class var, instead of a constant and we can override it.
self.commandtimeout = 0
# when we have some kind of result (valid, ot not) we call this handler
self.result_handler = on_result
# if it is True, we can safetely execute the command
self.canexecute = True
self.idlequeue = None
self.result = ''
self.endtime = None
self.pipe = None
def set_idlequeue(self, idlequeue):
self.idlequeue = idlequeue
def _return_result(self):
if self.result_handler:
self.result_handler(self.result)
self.result_handler = None
@staticmethod
def _compose_command_args():
return ['echo', 'da']
def _compose_command_line(self):
"""
Return one line representation of command and its arguments
"""
return ' '.join(self._compose_command_args())
def wait_child(self):
if self.pipe.poll() is None:
# result timeout
if self.endtime < self.idlequeue.current_time():
self._return_result()
self.pipe.stdout.close()
self.pipe.stdin.close()
else:
# child is still active, continue to wait
self.idlequeue.set_alarm(self.wait_child, 0.1)
else:
# child has quit
self.result = self.pipe.stdout.read()
self._return_result()
self.pipe.stdout.close()
self.pipe.stdin.close()
def start(self):
if not self.canexecute:
self.result = ''
self._return_result()
return
if os.name == 'nt':
self._start_nt()
elif os.name == 'posix':
self._start_posix()
def _start_nt(self):
# if program is started from noninteraactive shells stdin is closed and
# cannot be forwarded, so we have to keep it open
self.pipe = subprocess.Popen(self._compose_command_args(),
stdout=subprocess.PIPE,
bufsize=1024,
shell=True,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE)
if self.commandtimeout >= 0:
self.endtime = self.idlequeue.current_time() + self.commandtimeout
self.idlequeue.set_alarm(self.wait_child, 0.1)
def _start_posix(self):
self.pipe = os.popen(self._compose_command_line())
self.fd = self.pipe.fileno() # pylint: disable=no-member
fcntl.fcntl(self.pipe, fcntl.F_SETFL, os.O_NONBLOCK)
self.idlequeue.plug_idle(self, False, True)
if self.commandtimeout >= 0:
self.idlequeue.set_read_timeout(self.fd, self.commandtimeout)
def end(self):
self.idlequeue.unplug_idle(self.fd)
try:
self.pipe.close()
except Exception:
pass
def pollend(self):
self.idlequeue.remove_timeout(self.fd)
self.end()
self._return_result()
def pollin(self):
try:
res = self.pipe.read() # pylint: disable=no-member
except Exception:
res = ''
if res == '':
return self.pollend()
self.result += res
return None
def read_timeout(self):
self.end()
self._return_result()
class IdleQueue:
"""
IdleQueue provide three distinct time based features. Uses select.poll()
1. Alarm timeout: Execute a callback after foo seconds
2. Timeout event: Call read_timeout() of an plugged object if a timeout
has been set, but not removed in time.
3. Check file descriptor of plugged objects for read, write and error
events
"""
# (timeout, boolean)
# Boolean is True if timeout is specified in seconds,
# False means miliseconds
PROCESS_TIMEOUT = (100, False)
def __init__(self):
self.queue = {}
# when there is a timeout it executes obj.read_timeout()
# timeout is not removed automatically!
# {fd1: {timeout1: func1, timeout2: func2}}
# timout are unique (timeout1 must be != timeout2)
# If func1 is None, read_time function is called
self.read_timeouts = {}
# cb, which are executed after XX sec., alarms are removed automatically
self.alarms = {}
self._init_idle()
def _init_idle(self):
"""
Hook method for subclassed. Will be called by __init__
"""
self.selector = select.poll()
def set_alarm(self, alarm_cb, seconds):
"""
Set up a new alarm. alarm_cb will be called after specified seconds.
"""
alarm_time = self.current_time() + seconds
# almost impossible, but in case we have another alarm_cb at this time
if alarm_time in self.alarms:
self.alarms[alarm_time].append(alarm_cb)
else:
self.alarms[alarm_time] = [alarm_cb]
return alarm_time
def remove_alarm(self, alarm_cb, alarm_time):
"""
Remove alarm callback alarm_cb scheduled on alarm_time. Returns True if
it was removed sucessfully, otherwise False
"""
if alarm_time not in self.alarms:
return False
i = -1
for i in range(len(self.alarms[alarm_time])):
# let's not modify the list inside the loop
if self.alarms[alarm_time][i] is alarm_cb:
break
if i != -1:
del self.alarms[alarm_time][i]
if not self.alarms[alarm_time]:
del self.alarms[alarm_time]
return True
return False
def remove_timeout(self, fd, timeout=None):
"""
Remove the read timeout
"""
log.debug('read timeout removed for fd %s', fd)
if fd in self.read_timeouts:
if timeout:
if timeout in self.read_timeouts[fd]:
del self.read_timeouts[fd][timeout]
if len(self.read_timeouts[fd]) == 0:
del self.read_timeouts[fd]
else:
del self.read_timeouts[fd]
def set_read_timeout(self, fd, seconds, func=None):
"""
Seta a new timeout. If it is not removed after specified seconds,
func or obj.read_timeout() will be called
A filedescriptor fd can have several timeouts.
"""
log_txt = 'read timeout set for fd %s on %i seconds' % (fd, seconds)
if func:
log_txt += ' with function ' + str(func)
log.info(log_txt)
timeout = self.current_time() + seconds
if fd in self.read_timeouts:
self.read_timeouts[fd][timeout] = func
else:
self.read_timeouts[fd] = {timeout: func}
def _check_time_events(self):
"""
Execute and remove alarm callbacks and execute func() or read_timeout()
for plugged objects if specified time has ellapsed
"""
current_time = self.current_time()
for fd, timeouts in list(self.read_timeouts.items()):
if fd not in self.queue:
self.remove_timeout(fd)
continue
for timeout, func in list(timeouts.items()):
if timeout > current_time:
continue
if func:
log.debug('Calling %s for fd %s', func, fd)
func()
else:
log.debug('Calling read_timeout for fd %s', fd)
self.queue[fd].read_timeout()
self.remove_timeout(fd, timeout)
times = list(self.alarms.keys())
for alarm_time in times:
if alarm_time > current_time:
continue
if alarm_time in self.alarms:
for callback in self.alarms[alarm_time]:
callback()
if alarm_time in self.alarms:
del self.alarms[alarm_time]
def plug_idle(self, obj, writable=True, readable=True):
"""
Plug an IdleObject into idlequeue. Filedescriptor fd must be set
:param obj: the IdleObject
:param writable: True if obj has data to sent
:param readable: True if obj expects data to be received
"""
if obj.fd == -1:
return
if obj.fd in self.queue:
self.unplug_idle(obj.fd)
self.queue[obj.fd] = obj
if writable:
if not readable:
flags = FLAG_WRITE
else:
flags = FLAG_READ_WRITE
else:
if readable:
flags = FLAG_READ
else:
# when we paused a FT, we expect only a close event
flags = FLAG_CLOSE
self._add_idle(obj.fd, flags)
def _add_idle(self, fd, flags):
"""
Hook method for subclasses, called by plug_idle
"""
self.selector.register(fd, flags)
def unplug_idle(self, fd):
"""
Remove plugged IdleObject, specified by filedescriptor fd
"""
if fd in self.queue:
del self.queue[fd]
self._remove_idle(fd)
@staticmethod
def current_time():
return time.monotonic()
def _remove_idle(self, fd):
"""
Hook method for subclassed, called by unplug_idle
"""
self.selector.unregister(fd)
def _process_events(self, fd, flags):
obj = self.queue.get(fd)
if obj is None:
self.unplug_idle(fd)
return False
read_write = False
if flags & PENDING_READ:
#print 'waiting read on %d, flags are %d' % (fd, flags)
obj.pollin()
read_write = True
elif flags & PENDING_WRITE and not flags & IS_CLOSED:
obj.pollout()
read_write = True
if flags & IS_CLOSED:
# io error, don't expect more events
self.remove_timeout(obj.fd)
self.unplug_idle(obj.fd)
obj.pollend()
return False
if read_write:
return True
return False
def process(self):
"""
This function must be overridden by an implementation of the IdleQueue.
Process idlequeue. Check for any pending timeout or alarm events. Call
IdleObjects on possible and requested read, write and error events on
their file descriptors
Call this in regular intervals.
"""
raise NotImplementedError("You need to define a process() method.")
class SelectIdleQueue(IdleQueue):
"""
Extends IdleQueue to use select.select() for polling
This class exisists for the sake of gtk2.8 on windows, which doesn't seem to
support io_add_watch properly (yet)
"""
def checkQueue(self):
"""
Iterates through all known file descriptors and uses os.stat to
check if they're valid. Greatly improves performance if the caller
hands us and expects notification on an invalid file handle.
"""
bad_fds = []
union = {}
union.update(self.write_fds)
union.update(self.read_fds)
union.update(self.error_fds)
for fd in union:
try:
_status = os.stat(fd)
except OSError:
# This file descriptor is invalid. Add to list for closure.
bad_fds.append(fd)
for fd in bad_fds:
obj = self.queue.get(fd)
if obj is not None:
self.remove_timeout(fd)
self.unplug_idle(fd)
def _init_idle(self):
"""
Create a dict, which maps file/pipe/sock descriptor to glib event id
"""
self.read_fds = {}
self.write_fds = {}
self.error_fds = {}
def _add_idle(self, fd, flags):
"""
This method is called when we plug a new idle object. Add descriptor
to read/write/error lists, according flags
"""
if flags & 3:
self.read_fds[fd] = fd
if flags & 4:
self.write_fds[fd] = fd
self.error_fds[fd] = fd
def _remove_idle(self, fd):
"""
This method is called when we unplug a new idle object.
Remove descriptor from read/write/error lists
"""
if fd in self.read_fds:
del self.read_fds[fd]
if fd in self.write_fds:
del self.write_fds[fd]
if fd in self.error_fds:
del self.error_fds[fd]
def process(self):
if not self.write_fds and not self.read_fds:
self._check_time_events()
return True
try:
waiting_descriptors = select.select(
list(self.read_fds.keys()),
list(self.write_fds.keys()),
list(self.error_fds.keys()),
0)
except OSError as error:
waiting_descriptors = ((), (), ())
if error.errno != errno.EINTR:
self.checkQueue()
raise
for fd in waiting_descriptors[0]:
idle_object = self.queue.get(fd)
if idle_object:
idle_object.pollin()
for fd in waiting_descriptors[1]:
idle_object = self.queue.get(fd)
if idle_object:
idle_object.pollout()
for fd in waiting_descriptors[2]:
idle_object = self.queue.get(fd)
if idle_object:
idle_object.pollend()
self._check_time_events()
return True
class GlibIdleQueue(IdleQueue):
"""
Extends IdleQueue to use glib io_add_wath, instead of select/poll In another
'non gui' implementation of Gajim IdleQueue can be used safetly
"""
# (timeout, boolean)
# Boolean is True if timeout is specified in seconds,
# False means miliseconds
PROCESS_TIMEOUT = (2, True)
def _init_idle(self):
"""
Creates a dict, which maps file/pipe/sock descriptor to glib event id
"""
self.events = {}
def _add_idle(self, fd, flags):
"""
This method is called when we plug a new idle object.
Start listening for events from fd
"""
res = GLib.io_add_watch(fd,
GLib.PRIORITY_LOW,
flags,
self._process_events)
# store the id of the watch, so that we can remove it on unplug
self.events[fd] = res
def _process_events(self, fd, flags):
try:
return IdleQueue._process_events(self, fd, flags)
except Exception:
self._remove_idle(fd)
self._add_idle(fd, flags)
raise
def _remove_idle(self, fd):
"""
This method is called when we unplug a new idle object. Stop listening
for events from fd
"""
if not fd in self.events:
return
GLib.source_remove(self.events[fd])
del self.events[fd]
def process(self):
self._check_time_events()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/ 0000775 0000000 0000000 00000000000 14130341156 0020614 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/__init__.py 0000664 0000000 0000000 00000000000 14130341156 0022713 0 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/activity.py 0000664 0000000 0000000 00000007153 14130341156 0023030 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import ActivityData
from nbxmpp.const import ACTIVITIES
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
from nbxmpp.task import iq_request_task
class Activity(BaseModule):
_depends = {
'publish': 'PubSub'
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_activity,
ns=Namespace.PUBSUB_EVENT,
priority=16),
]
def _process_pubsub_activity(self, _client, stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.ACTIVITY:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
activity_node = item.getTag('activity', namespace=Namespace.ACTIVITY)
if not activity_node.getChildren():
self._log.info('Received activity: %s - no activity set',
properties.jid)
return
activity, subactivity, text = None, None, None
for child in activity_node.getChildren():
name = child.getName()
if name == 'text':
text = child.getData()
elif name in ACTIVITIES:
activity = name
subactivity = self._parse_sub_activity(child)
if activity is None and activity_node.getPayload():
self._log.warning('No valid activity value found')
self._log.warning(stanza)
raise NodeProcessed
data = ActivityData(activity, subactivity, text)
pubsub_event = properties.pubsub_event._replace(data=data)
self._log.info('Received activity: %s - %s', properties.jid, data)
properties.pubsub_event = pubsub_event
@staticmethod
def _parse_sub_activity(activity):
sub_activities = ACTIVITIES[activity.getName()]
for sub in activity.getChildren():
if sub.getName() in sub_activities:
return sub.getName()
return None
@iq_request_task
def set_activity(self, data):
task = yield
item = Node('activity', {'xmlns': Namespace.ACTIVITY})
if data is not None and data.activity:
activity_node = item.addChild(data.activity)
if data.subactivity:
activity_node.addChild(data.subactivity)
if data.text:
item.addChild('text', payload=data.text)
result = yield self.publish(Namespace.ACTIVITY, item, id_='current')
yield finalize(task, result)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/adhoc.py 0000664 0000000 0000000 00000015624 14130341156 0022254 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Node
from nbxmpp.structs import AdHocCommand
from nbxmpp.structs import AdHocCommandNote
from nbxmpp.const import AdHocStatus
from nbxmpp.const import AdHocAction
from nbxmpp.const import AdHocNoteType
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.discovery import get_disco_request
from nbxmpp.modules.base import BaseModule
class AdHoc(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def request_command_list(self, jid=None):
_task = yield
if jid is None:
jid = self._client.get_bound_jid().bare
response = yield get_disco_request(Namespace.DISCO_ITEMS,
jid,
node=Namespace.COMMANDS)
if response.isError():
raise StanzaError(response)
payload = response.getQueryPayload()
if payload is None:
raise MalformedStanzaError('query payload missing', response)
command_list = []
for item in payload:
if item.getName() != 'item':
continue
try:
command_list.append(AdHocCommand(**item.getAttrs()))
except Exception as error:
raise MalformedStanzaError(f'invalid item attributes: {error}',
response)
yield command_list
@iq_request_task
def execute_command(self, cmd, action=None, dataform=None):
_task = yield
if action is None:
action = AdHocAction.EXECUTE
attrs = {'node': cmd.node,
'xmlns': Namespace.COMMANDS,
'action': action.value}
if cmd.sessionid is not None:
attrs['sessionid'] = cmd.sessionid
response = yield _make_command(cmd, attrs, dataform)
if response.isError():
raise StanzaError(response)
command = response.getTag('command', namespace=Namespace.COMMANDS)
if command is None:
raise MalformedStanzaError('command node missing', response)
node = command.getAttr('node')
if node is None:
raise MalformedStanzaError('node attribute missing', response)
status = command.getAttr('status')
if status is None:
raise MalformedStanzaError('status attribute missing', response)
if status not in ('executing', 'completed', 'canceled'):
raise MalformedStanzaError('invalid status attribute %s' % status,
response)
status = AdHocStatus(status)
sessionid = command.getAttr('sessionid')
if sessionid is None and _expect_sessionid(status, cmd.sessionid):
raise MalformedStanzaError('sessionid attribute missing', response)
try:
notes = _parse_notes(command)
except ValueError as error:
raise MalformedStanzaError(error, response)
try:
actions, default = _parse_actions(command)
except ValueError as error:
raise MalformedStanzaError(error, response)
yield AdHocCommand(
jid=response.getFrom(),
name=None,
node=node,
sessionid=sessionid,
status=status,
data=command.getTag('x', namespace=Namespace.DATA),
actions=actions,
default=default,
notes=notes)
def _make_command(command, attrs, dataform):
command_node = Node('command', attrs=attrs)
if dataform is not None:
command_node.addChild(node=dataform)
iq = Iq('set', to=command.jid)
iq.addChild(node=command_node)
return iq
def _parse_notes(command):
notes = []
for note in command.getTags('note'):
type_ = note.getAttr('type')
if type_ is None:
type_ = 'info'
if type_ not in ('info', 'warn', 'error'):
raise ValueError('invalid note type %s' % type_)
notes.append(AdHocCommandNote(text=note.getData(),
type=AdHocNoteType(type_)))
return notes
def _parse_actions(command):
if command.getAttr('status') != 'executing':
return set(), None
actions_node = command.getTag('actions')
if actions_node is None:
# If there is no element,
# the user-agent can use a single-stage dialog or view.
# The action "execute" is equivalent to the action "complete".
return {AdHocAction.CANCEL, AdHocAction.COMPLETE}, AdHocAction.COMPLETE
default = actions_node.getAttr('execute')
if default is None:
# If the "execute" attribute is absent, it defaults to "next".
default = 'next'
if default not in ('prev', 'next', 'complete'):
raise ValueError('invalid execute attribute %s' % default)
default = AdHocAction(default)
# We use a set because it cannot contain duplicates
actions = set()
for action in actions_node.getChildren():
name = action.getName()
if name == 'execute':
actions.add(default)
if name in ('prev', 'next', 'complete'):
actions.add(AdHocAction(name))
if not actions:
raise ValueError('actions element without actions')
# The action "cancel" is always allowed.
actions.add(AdHocAction.CANCEL)
# A form which has an element and an "execute" attribute
# which evaluates (taking the default into account if absent) to an
# action which is not allowed is therefore invalid.
if default not in actions:
# Some implementations don’t respect this rule.
# Take the first action so we don’t fail here.
for act in actions:
default = act
break
return actions, default
def _expect_sessionid(status, sent_sessionid):
# Session id should only be expected for multiple stage commands
# or when we initialize the session (set the session attribute)
return status != status.COMPLETED or sent_sessionid is not None
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/annotations.py 0000664 0000000 0000000 00000007142 14130341156 0023527 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Node
from nbxmpp.protocol import JID
from nbxmpp.structs import AnnotationNote
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import process_response
from nbxmpp.modules.date_and_time import parse_datetime
class Annotations(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@property
def domain(self):
return self._client.get_bound_jid().domain
@iq_request_task
def request_annotations(self):
_task = yield
response = yield _make_request()
if response.isError():
raise StanzaError(response)
query = response.getQuery()
storage = query.getTag('storage', namespace=Namespace.ROSTERNOTES)
if storage is None:
raise MalformedStanzaError('storage node missing', response)
notes = []
for note in storage.getTags('note'):
try:
jid = JID.from_string(note.getAttr('jid'))
except Exception as error:
self._log.warning('Invalid JID: %s, %s',
note.getAttr('jid'), error)
continue
cdate = note.getAttr('cdate')
if cdate is not None:
cdate = parse_datetime(cdate, epoch=True)
mdate = note.getAttr('mdate')
if mdate is not None:
mdate = parse_datetime(mdate, epoch=True)
data = note.getData()
notes.append(AnnotationNote(jid=jid, cdate=cdate,
mdate=mdate, data=data))
self._log.info('Received annotations from %s:', self.domain)
for note in notes:
self._log.info(note)
yield notes
@iq_request_task
def set_annotations(self, notes):
_task = yield
self._log.info('Set annotations for %s:', self.domain)
for note in notes:
self._log.info(note)
response = yield _make_set_request(notes)
yield process_response(response)
def _make_request():
payload = Node('storage', attrs={'xmlns': Namespace.ROSTERNOTES})
return Iq(typ='get', queryNS=Namespace.PRIVATE, payload=payload)
def _make_set_request(notes):
storage = Node('storage', attrs={'xmlns': Namespace.ROSTERNOTES})
for note in notes:
node = Node('note', attrs={'jid': note.jid})
node.setData(note.data)
if note.cdate is not None:
node.setAttr('cdate', note.cdate)
if note.mdate is not None:
node.setAttr('mdate', note.mdate)
storage.addChild(node=node)
return Iq(typ='set', queryNS=Namespace.PRIVATE, payload=storage)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/attention.py 0000664 0000000 0000000 00000003252 14130341156 0023175 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.base import BaseModule
class Attention(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_attention,
ns=Namespace.ATTENTION,
priority=15),
]
def _process_message_attention(self, _client, stanza, properties):
attention = stanza.getTag('attention', namespace=Namespace.ATTENTION)
if attention is None:
return
if properties.is_mam_message:
return
if properties.is_carbon_message and properties.carbon.is_sent:
return
if stanza.getTag('delay', namespace=Namespace.DELAY2) is not None:
return
properties.attention = True
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/base.py 0000664 0000000 0000000 00000002404 14130341156 0022100 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from nbxmpp.util import LogAdapter
class BaseModule:
_depends = {}
def __init__(self, client):
logger_name = 'nbxmpp.m.%s' % self.__class__.__name__.lower()
self._log = LogAdapter(logging.getLogger(logger_name),
{'context': client.log_context})
def __getattr__(self, name):
if name not in self._depends:
raise AttributeError('Unknown method: %s' % name)
module = self._client.get_module(self._depends[name])
return getattr(module, name)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/bits_of_binary.py 0000664 0000000 0000000 00000004571 14130341156 0024166 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
import hashlib
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import BobData
from nbxmpp.util import b64decode
log = logging.getLogger('nbxmpp.m.bob')
def parse_bob_data(stanza):
data_node = stanza.getTag('data', namespace=Namespace.BOB)
if data_node is None:
return None
cid = data_node.getAttr('cid')
type_ = data_node.getAttr('type')
max_age = data_node.getAttr('max-age')
if max_age is not None:
try:
max_age = int(max_age)
except Exception:
log.exception(stanza)
return None
if cid is None or type_ is None:
log.warning('Invalid data node (no cid or type attr): %s', stanza)
return None
try:
algo_hash = cid.split('@')[0]
algo, hash_ = algo_hash.split('+')
except Exception:
log.exception('Invalid cid: %s', stanza)
return None
bob_data = data_node.getData()
if not bob_data:
log.warning('No bob data found: %s', stanza)
return None
try:
bob_data = b64decode(bob_data, return_type=bytes)
except Exception:
log.warning('Unable to decode data')
log.exception(stanza)
return None
try:
sha = hashlib.new(algo)
except ValueError as error:
log.warning(stanza)
log.warning(error)
return None
sha.update(bob_data)
if sha.hexdigest() != hash_:
log.warning('Invalid hash: %s', stanza)
return None
return BobData(algo=algo,
hash_=hash_,
max_age=max_age,
data=bob_data,
cid=cid,
type=type_)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/blocking.py 0000664 0000000 0000000 00000010404 14130341156 0022755 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.protocol import JID
from nbxmpp.modules.base import BaseModule
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.structs import BlockingPush
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.util import process_response
class Blocking(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='iq',
priority=15,
callback=self._process_blocking_push,
typ='set',
ns=Namespace.BLOCKING),
]
@iq_request_task
def request_blocking_list(self):
_task = yield
result = yield _make_blocking_list_request()
if result.isError():
raise StanzaError(result)
blocklist = result.getTag('blocklist', namespace=Namespace.BLOCKING)
if blocklist is None:
raise MalformedStanzaError('blocklist node missing', result)
blocked = []
for item in blocklist.getTags('item'):
blocked.append(item.getAttr('jid'))
self._log.info('Received blocking list: %s', blocked)
yield blocked
@iq_request_task
def block(self, jids, report=None):
_task = yield
self._log.info('Block: %s', jids)
response = yield _make_block_request(jids, report)
yield process_response(response)
@iq_request_task
def unblock(self, jids):
_task = yield
self._log.info('Unblock: %s', jids)
response = yield _make_unblock_request(jids)
yield process_response(response)
@staticmethod
def _process_blocking_push(client, stanza, properties):
unblock = stanza.getTag('unblock', namespace=Namespace.BLOCKING)
if unblock is not None:
properties.blocking = _parse_push(unblock)
return
block = stanza.getTag('block', namespace=Namespace.BLOCKING)
if block is not None:
properties.blocking = _parse_push(block)
reply = stanza.buildSimpleReply('result')
client.send_stanza(reply)
def _make_blocking_list_request():
iq = Iq('get', Namespace.BLOCKING)
iq.setQuery('blocklist')
return iq
def _make_block_request(jids, report):
iq = Iq('set', Namespace.BLOCKING)
query = iq.setQuery(name='block')
for jid in jids:
item = query.addChild(name='item', attrs={'jid': jid})
if report in ('spam', 'abuse'):
action = item.addChild(name='report',
namespace=Namespace.REPORTING)
action.setTag(report)
return iq
def _make_unblock_request(jids):
iq = Iq('set', Namespace.BLOCKING)
query = iq.setQuery(name='unblock')
for jid in jids:
query.addChild(name='item', attrs={'jid': jid})
return iq
def _parse_push(node):
items = node.getTags('item')
if not items:
return BlockingPush(block=[], unblock=[], unblock_all=True)
jids = []
for item in items:
jid = item.getAttr('jid')
if not jid:
continue
try:
jid = JID.from_string(jid)
except Exception:
continue
jids.append(jid)
block, unblock = [], []
if node.getName() == 'block':
block = jids
else:
unblock = jids
return BlockingPush(block=block, unblock=unblock, unblock_all=False)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/bookmarks/ 0000775 0000000 0000000 00000000000 14130341156 0022604 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/bookmarks/__init__.py 0000664 0000000 0000000 00000000000 14130341156 0024703 0 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/bookmarks/native_bookmarks.py 0000664 0000000 0000000 00000007733 14130341156 0026526 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.task import iq_request_task
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import raise_if_error
from nbxmpp.modules.util import finalize
from nbxmpp.modules.bookmarks.util import parse_bookmark
from nbxmpp.modules.bookmarks.util import build_conference_node
BOOKMARK_OPTIONS = {
'pubsub#notify_delete': 'true',
'pubsub#notify_retract': 'true',
'pubsub#persist_items': 'true',
'pubsub#max_items': 'max',
'pubsub#access_model': 'whitelist',
'pubsub#send_last_published_item': 'never',
}
class NativeBookmarks(BaseModule):
_depends = {
'retract': 'PubSub',
'publish': 'PubSub',
'request_items': 'PubSub',
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_bookmarks,
ns=Namespace.PUBSUB_EVENT,
priority=16),
]
def _process_pubsub_bookmarks(self, _client, _stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.BOOKMARKS_1:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
try:
bookmark_item = parse_bookmark(item)
except MalformedStanzaError as error:
self._log.warning(error)
self._log.warning(error.stanza)
raise NodeProcessed
pubsub_event = properties.pubsub_event._replace(data=bookmark_item)
self._log.info('Received bookmark item from: %s', properties.jid)
self._log.info(bookmark_item)
properties.pubsub_event = pubsub_event
@iq_request_task
def request_bookmarks(self):
_task = yield
items = yield self.request_items(Namespace.BOOKMARKS_1)
raise_if_error(items)
bookmarks = []
for item in items:
try:
bookmark_item = parse_bookmark(item)
except MalformedStanzaError as error:
self._log.warning(error)
self._log.warning(error.stanza)
continue
bookmarks.append(bookmark_item)
for bookmark in bookmarks:
self._log.info(bookmark)
yield bookmarks
@iq_request_task
def retract_bookmark(self, bookmark_jid):
task = yield
self._log.info('Retract Bookmark: %s', bookmark_jid)
result = yield self.retract(Namespace.BOOKMARKS_1, str(bookmark_jid))
yield finalize(task, result)
@iq_request_task
def store_bookmarks(self, bookmarks):
_task = yield
self._log.info('Store Bookmarks')
for bookmark in bookmarks:
self.publish(Namespace.BOOKMARKS_1,
build_conference_node(bookmark),
id_=str(bookmark.jid),
options=BOOKMARK_OPTIONS,
force_node_options=True)
yield True
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/bookmarks/pep_bookmarks.py 0000664 0000000 0000000 00000006463 14130341156 0026023 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.task import iq_request_task
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import raise_if_error
from nbxmpp.modules.bookmarks.util import parse_bookmarks
from nbxmpp.modules.bookmarks.util import build_storage_node
BOOKMARK_OPTIONS = {
'pubsub#persist_items': 'true',
'pubsub#access_model': 'whitelist',
}
class PEPBookmarks(BaseModule):
_depends = {
'publish': 'PubSub',
'request_items': 'PubSub',
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_bookmarks,
ns=Namespace.PUBSUB_EVENT,
priority=16),
]
def _process_pubsub_bookmarks(self, _client, stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.BOOKMARKS:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
try:
bookmarks = parse_bookmarks(item, self._log)
except MalformedStanzaError as error:
self._log.warning(error)
self._log.warning(stanza)
raise NodeProcessed
if not bookmarks:
self._log.info('Bookmarks removed')
return
pubsub_event = properties.pubsub_event._replace(data=bookmarks)
self._log.info('Received bookmarks from: %s', properties.jid)
for bookmark in bookmarks:
self._log.info(bookmark)
properties.pubsub_event = pubsub_event
@iq_request_task
def request_bookmarks(self):
_task = yield
items = yield self.request_items(Namespace.BOOKMARKS, max_items=1)
raise_if_error(items)
if not items:
yield []
bookmarks = parse_bookmarks(items[0], self._log)
for bookmark in bookmarks:
self._log.info(bookmark)
yield bookmarks
@iq_request_task
def store_bookmarks(self, bookmarks):
_task = yield
self._log.info('Store Bookmarks')
self.publish(Namespace.BOOKMARKS,
build_storage_node(bookmarks),
id_='current',
options=BOOKMARK_OPTIONS,
force_node_options=True)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/bookmarks/private_bookmarks.py 0000664 0000000 0000000 00000003706 14130341156 0026706 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.task import iq_request_task
from nbxmpp.errors import StanzaError
from nbxmpp.modules.util import process_response
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.bookmarks.util import build_storage_node
from nbxmpp.modules.bookmarks.util import get_private_request
from nbxmpp.modules.bookmarks.util import parse_private_bookmarks
class PrivateBookmarks(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def request_bookmarks(self):
_task = yield
response = yield get_private_request()
if response.isError():
raise StanzaError(response)
bookmarks = parse_private_bookmarks(response, self._log)
for bookmark in bookmarks:
self._log.info(bookmark)
yield bookmarks
@iq_request_task
def store_bookmarks(self, bookmarks):
_task = yield
self._log.info('Store Bookmarks')
storage_node = build_storage_node(bookmarks)
response = yield Iq('set', Namespace.PRIVATE, payload=storage_node)
yield process_response(response)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/bookmarks/util.py 0000664 0000000 0000000 00000011405 14130341156 0024134 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.protocol import Node
from nbxmpp.protocol import validate_resourcepart
from nbxmpp.protocol import JID
from nbxmpp.protocol import Iq
from nbxmpp.util import from_xs_boolean
from nbxmpp.util import to_xs_boolean
from nbxmpp.namespaces import Namespace
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.structs import BookmarkData
def parse_nickname(nick):
if nick is None:
return None
try:
return validate_resourcepart(nick)
except Exception:
return None
def parse_autojoin(autojoin):
if autojoin is None:
return False
try:
return from_xs_boolean(autojoin)
except ValueError:
return False
def parse_bookmark(item):
conference = item.getTag('conference', namespace=Namespace.BOOKMARKS_1)
if conference is None:
raise MalformedStanzaError('conference node missing', item)
try:
jid = JID.from_string(item.getAttr('id'))
except Exception as error:
raise MalformedStanzaError('invalid jid: %s' % error, item)
if jid.localpart is None or jid.resource is not None:
raise MalformedStanzaError('invalid jid', item)
autojoin = parse_autojoin(conference.getAttr('autojoin'))
nick = parse_nickname(conference.getTagData('nick'))
name = conference.getAttr('name') or None
password = conference.getTagData('password') or None
return BookmarkData(jid=jid,
name=name,
autojoin=autojoin,
password=password,
nick=nick)
def parse_bookmarks(item, log):
storage_node = item.getTag('storage', namespace=Namespace.BOOKMARKS)
if storage_node is None:
raise MalformedStanzaError('storage node missing', item)
return parse_storage_node(storage_node, log)
def parse_private_bookmarks(response, log):
query = response.getQuery()
storage_node = query.getTag('storage', namespace=Namespace.BOOKMARKS)
if storage_node is None:
raise MalformedStanzaError('storage node missing', response)
return parse_storage_node(storage_node, log)
def parse_storage_node(storage, log):
bookmarks = []
confs = storage.getTags('conference')
for conf in confs:
try:
jid = JID.from_string(conf.getAttr('jid'))
except Exception:
log.warning('invalid jid: %s', conf)
continue
if jid.localpart is None or jid.resource is not None:
log.warning('invalid jid: %s', conf)
continue
autojoin = parse_autojoin(conf.getAttr('autojoin'))
nick = parse_nickname(conf.getTagData('nick'))
name = conf.getAttr('name') or None
password = conf.getTagData('password') or None
bookmark = BookmarkData(
jid=jid,
name=name,
autojoin=autojoin,
password=password,
nick=nick)
bookmarks.append(bookmark)
return bookmarks
def build_conference_node(bookmark):
attrs = {'xmlns': Namespace.BOOKMARKS_1}
if bookmark.autojoin:
attrs['autojoin'] = 'true'
if bookmark.name:
attrs['name'] = bookmark.name
conference = Node(tag='conference', attrs=attrs)
if bookmark.nick:
conference.setTagData('nick', bookmark.nick)
return conference
def build_storage_node(bookmarks):
storage_node = Node(tag='storage', attrs={'xmlns': Namespace.BOOKMARKS})
for bookmark in bookmarks:
conf_node = storage_node.addChild(name="conference")
conf_node.setAttr('jid', bookmark.jid)
conf_node.setAttr('autojoin', to_xs_boolean(bookmark.autojoin))
if bookmark.name:
conf_node.setAttr('name', bookmark.name)
if bookmark.nick:
conf_node.setTagData('nick', bookmark.nick)
if bookmark.password:
conf_node.setTagData('password', bookmark.password)
return storage_node
def get_private_request():
iq = Iq(typ='get')
query = iq.addChild(name='query', namespace=Namespace.PRIVATE)
query.addChild(name='storage', namespace=Namespace.BOOKMARKS)
return iq
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/captcha.py 0000664 0000000 0000000 00000003631 14130341156 0022574 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import CaptchaData
from nbxmpp.modules.dataforms import extend_form
from nbxmpp.modules.bits_of_binary import parse_bob_data
from nbxmpp.modules.base import BaseModule
class Captcha(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_captcha,
ns=Namespace.CAPTCHA,
priority=40),
]
def _process_captcha(self, _client, stanza, properties):
captcha = stanza.getTag('captcha', namespace=Namespace.CAPTCHA)
if captcha is None:
return
data_form = captcha.getTag('x', namespace=Namespace.DATA)
if data_form is None:
self._log.warning('Invalid captcha form')
self._log.warning(stanza)
return
form = extend_form(node=data_form)
bob_data = parse_bob_data(stanza)
properties.captcha = CaptchaData(form=form,
bob_data=bob_data)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/chat_markers.py 0000664 0000000 0000000 00000003700 14130341156 0023631 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import ChatMarker
from nbxmpp.modules.base import BaseModule
class ChatMarkers(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_marker,
ns=Namespace.CHATMARKERS,
priority=15),
]
def _process_message_marker(self, _client, stanza, properties):
type_ = stanza.getTag('received', namespace=Namespace.CHATMARKERS)
if type_ is None:
type_ = stanza.getTag('displayed', namespace=Namespace.CHATMARKERS)
if type_ is None:
type_ = stanza.getTag('acknowledged',
namespace=Namespace.CHATMARKERS)
if type_ is None:
return
name = type_.getName()
id_ = type_.getAttr('id')
if id_ is None:
self._log.warning('Chatmarker without id')
self._log.warning(stanza)
return
properties.marker = ChatMarker(name, id_)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/chatstates.py 0000664 0000000 0000000 00000003706 14130341156 0023337 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import CHATSTATES
from nbxmpp.modules.base import BaseModule
class Chatstates(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_chatstate,
ns=Namespace.CHATSTATES,
priority=15),
]
def _process_message_chatstate(self, _client, stanza, properties):
chatstate = parse_chatstate(stanza)
if chatstate is None:
return
if properties.is_mam_message:
return
if stanza.getTag('delay', namespace=Namespace.DELAY2) is not None:
return
if chatstate not in CHATSTATES:
self._log.warning('Invalid chatstate: %s', chatstate)
self._log.warning(stanza)
return
properties.chatstate = chatstate
def parse_chatstate(stanza):
children = stanza.getChildren()
for child in children:
if child.getNamespace() == Namespace.CHATSTATES:
return child.getName()
return None
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/correction.py 0000664 0000000 0000000 00000003467 14130341156 0023347 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import CorrectionData
from nbxmpp.modules.base import BaseModule
class Correction(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_correction,
ns=Namespace.CORRECT,
priority=15),
]
def _process_message_correction(self, _client, stanza, properties):
replace = stanza.getTag('replace', namespace=Namespace.CORRECT)
if replace is None:
return
id_ = replace.getAttr('id')
if id_ is None:
self._log.warning('Correcton without id attribute')
self._log.warning(stanza)
return
if stanza.getID() == id_:
self._log.warning('correcton id == message id')
self._log.warning(stanza)
return
properties.correction = CorrectionData(id_)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/dataforms.py 0000664 0000000 0000000 00000047366 14130341156 0023166 0 ustar 00root root 0000000 0000000 # Copyright (C) 2006-2007 Tomasz Melcer
# Copyright (C) 2006-2014 Yann Leboulanger
# Copyright (C) 2007 Stephan Erb
# Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# nbxmpp is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# nbxmpp is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with nbxmpp. If not, see .
# XEP-0004: Data Forms
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.simplexml import Node
# exceptions used in this module
class Error(Exception):
pass
# when we get nbxmpp.Node which we do not understand
class UnknownDataForm(Error):
pass
# when we get nbxmpp.Node which contains bad fields
class WrongFieldValue(Error):
pass
# helper class to change class of already existing object
class ExtendedNode(Node):
@classmethod
def __new__(cls, *args, **kwargs):
if 'extend' not in kwargs.keys() or not kwargs['extend']:
return object.__new__(cls)
extend = kwargs['extend']
assert issubclass(cls, extend.__class__)
extend.__class__ = cls
return extend
# helper to create fields from scratch
def create_field(typ, **attrs):
''' Helper function to create a field of given type. '''
field = {
'boolean': BooleanField,
'fixed': StringField,
'hidden': StringField,
'text-private': StringField,
'text-single': StringField,
'jid-multi': JidMultiField,
'jid-single': JidSingleField,
'list-multi': ListMultiField,
'list-single': ListSingleField,
'text-multi': TextMultiField,
}[typ](typ=typ, **attrs)
return field
def extend_field(node):
"""
Helper function to extend a node to field of appropriate type
"""
# when validation (XEP-122) will go in, we could have another classes
# like DateTimeField - so that dicts in create_field() and
# extend_field() will be different...
typ = node.getAttr('type')
field = {
'boolean': BooleanField,
'fixed': StringField,
'hidden': StringField,
'text-private': StringField,
'text-single': StringField,
'jid-multi': JidMultiField,
'jid-single': JidSingleField,
'list-multi': ListMultiField,
'list-single': ListSingleField,
'text-multi': TextMultiField,
}
if typ not in field:
typ = 'text-single'
return field[typ](extend=node)
def extend_form(node):
"""
Helper function to extend a node to form of appropriate type
"""
if node.getTag('reported') is not None:
return MultipleDataForm(extend=node)
return SimpleDataForm(extend=node)
class DataField(ExtendedNode):
"""
Keeps data about one field - var, field type, labels, instructions... Base
class for different kinds of fields. Use create_field() function to
construct one of these
"""
def __init__(self, typ=None, var=None, value=None, label=None, desc=None,
required=False, options=None, extend=None):
if extend is None:
ExtendedNode.__init__(self, 'field')
self.type_ = typ
self.var = var
if value is not None:
self.value = value
if label is not None:
self.label = label
if desc is not None:
self.desc = desc
self.required = required
self.options = options
@property
def type_(self):
"""
Type of field. Recognized values are: 'boolean', 'fixed', 'hidden',
'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi',
'text-private', 'text-single'. If you set this to something different,
DataField will store given name, but treat all data as text-single
"""
type_ = self.getAttr('type')
if type_ is None:
return 'text-single'
return type_
@type_.setter
def type_(self, value):
assert isinstance(value, str)
self.setAttr('type', value)
@property
def var(self):
"""
Field identifier
"""
return self.getAttr('var')
@var.setter
def var(self, value):
assert isinstance(value, str)
self.setAttr('var', value)
@var.deleter
def var(self):
self.delAttr('var')
@property
def label(self):
"""
Human-readable field name
"""
label_ = self.getAttr('label')
if not label_:
label_ = self.var
return label_
@label.setter
def label(self, value):
assert isinstance(value, str)
self.setAttr('label', value)
@label.deleter
def label(self):
if self.getAttr('label'):
self.delAttr('label')
@property
def description(self):
"""
Human-readable description of field meaning
"""
return self.getTagData('desc') or ''
@description.setter
def description(self, value):
assert isinstance(value, str)
if value == '':
del self.description
else:
self.setTagData('desc', value)
@description.deleter
def description(self):
desc = self.getTag('desc')
if desc is not None:
self.delChild(desc)
@property
def required(self):
"""
Controls whether this field required to fill. Boolean
"""
return bool(self.getTag('required'))
@required.setter
def required(self, value):
required = self.getTag('required')
if required and not value:
self.delChild(required)
elif not required and value:
self.addChild('required')
@property
def media(self):
"""
Media data
"""
media = self.getTag('media', namespace=Namespace.DATA_MEDIA)
if media:
return Media(media)
return None
@media.setter
def media(self, value):
del self.media
self.addChild(node=value)
@media.deleter
def media(self):
media = self.getTag('media')
if media is not None:
self.delChild(media)
@staticmethod
def is_valid():
return True, ''
class Uri(Node):
def __init__(self, uri_tag):
Node.__init__(self, node=uri_tag)
@property
def type_(self):
"""
uri type
"""
return self.getAttr('type')
@type_.setter
def type_(self, value):
self.setAttr('type', value)
@type_.deleter
def type_(self):
self.delAttr('type')
@property
def uri_data(self):
"""
uri data
"""
return self.getData()
@uri_data.setter
def uri_data(self, value):
self.setData(value)
@uri_data.deleter
def uri_data(self):
self.setData(None)
class Media(Node):
def __init__(self, media_tag):
Node.__init__(self, node=media_tag)
@property
def uris(self):
"""
URIs of the media element.
"""
return map(Uri, self.getTags('uri'))
@uris.setter
def uris(self, value):
del self.uris
for uri in value:
self.addChild(node=uri)
@uris.deleter
def uris(self):
for element in self.getTags('uri'):
self.delChild(element)
class BooleanField(DataField):
@property
def value(self):
"""
Value of field. May contain True, False or None
"""
value = self.getTagData('value')
if value in ('0', 'false'):
return False
if value in ('1', 'true'):
return True
if value is None:
return False # default value is False
raise WrongFieldValue
@value.setter
def value(self, value):
self.setTagData('value', value and '1' or '0')
@value.deleter
def value(self):
value = self.getTag('value')
if value is not None:
self.delChild(value)
class StringField(DataField):
"""
Covers fields of types: fixed, hidden, text-private, text-single
"""
@property
def value(self):
"""
Value of field. May be any string
"""
return self.getTagData('value') or ''
@value.setter
def value(self, value):
if value is None:
value = ''
self.setTagData('value', value)
@value.deleter
def value(self):
try:
self.delChild(self.getTag('value'))
except ValueError: # if there already were no value tag
pass
def is_valid(self):
if not self.required:
return True, ''
if not self.value:
return False, ''
return True, ''
class ListField(DataField):
"""
Covers fields of types: jid-multi, jid-single, list-multi, list-single
"""
@property
def options(self):
"""
Options
"""
options = []
for element in self.getTags('option'):
value = element.getTagData('value')
if value is None:
raise WrongFieldValue
label = element.getAttr('label')
if not label:
label = value
options.append((label, value))
return options
@options.setter
def options(self, values):
del self.options
for value, label in values:
self.addChild('option',
{'label': label}).setTagData('value', value)
@options.deleter
def options(self):
for element in self.getTags('option'):
self.delChild(element)
def iter_options(self):
for element in self.iterTags('option'):
value = element.getTagData('value')
if value is None:
raise WrongFieldValue
label = element.getAttr('label')
if not label:
label = value
yield (value, label)
class ListSingleField(ListField, StringField):
"""
Covers list-single field
"""
def is_valid(self):
if not self.required:
return True, ''
if not self.value:
return False, ''
return True, ''
class JidSingleField(ListSingleField):
"""
Covers jid-single fields
"""
def is_valid(self):
if self.value:
try:
JID.from_string(self.value)
return True, ''
except Exception as error:
return False, error
if self.required:
return False, ''
return True, ''
class ListMultiField(ListField):
"""
Covers list-multi fields
"""
@property
def values(self):
"""
Values held in field
"""
values = []
for element in self.getTags('value'):
values.append(element.getData())
return values
@values.setter
def values(self, values):
del self.values
for value in values:
self.addChild('value').setData(value)
@values.deleter
def values(self):
for element in self.getTags('value'):
self.delChild(element)
def iter_values(self):
for element in self.getTags('value'):
yield element.getData()
def is_valid(self):
if not self.required:
return True, ''
if not self.values:
return False, ''
return True, ''
class JidMultiField(ListMultiField):
"""
Covers jid-multi fields
"""
def is_valid(self):
if self.values:
for value in self.values:
try:
JID.from_string(value)
except Exception as error:
return False, error
return True, ''
if self.required:
return False, ''
return True, ''
class TextMultiField(DataField):
@property
def value(self):
"""
Value held in field
"""
value = ''
for element in self.iterTags('value'):
value += '\n' + element.getData()
return value[1:]
@value.setter
def value(self, value):
del self.value
if value == '':
return
for line in value.split('\n'):
self.addChild('value').setData(line)
@value.deleter
def value(self):
for element in self.getTags('value'):
self.delChild(element)
def is_valid(self):
if not self.required:
return True, ''
if not self.value:
return False, ''
return True, ''
class DataRecord(ExtendedNode):
"""
The container for data fields - an xml element which has DataField elements
as children
"""
def __init__(self, fields=None, associated=None, extend=None):
self.associated = associated
self.vars = {}
if extend is None:
# we have to build this object from scratch
Node.__init__(self)
if fields is not None:
self.fields = fields
else:
# we already have nbxmpp.Node inside - try to convert all
# fields into DataField objects
if fields is None:
for field in self.iterTags('field'):
if not isinstance(field, DataField):
extend_field(field)
self.vars[field.var] = field
else:
self.fields = fields
@property
def fields(self):
"""
List of fields in this record
"""
return self.getTags('field')
@fields.setter
def fields(self, fields):
del self.fields
for field in fields:
if not isinstance(field, DataField):
extend_field(field)
self.addChild(node=field)
self.vars[field.var] = field
@fields.deleter
def fields(self):
for element in self.getTags('field'):
self.delChild(element)
self.vars.clear()
def iter_fields(self):
"""
Iterate over fields in this record. Do not take associated into account
"""
for field in self.iterTags('field'):
yield field
def iter_with_associated(self):
"""
Iterate over associated, yielding both our field and associated one
together
"""
for field in self.associated.iter_fields():
yield self[field.var], field
def __getitem__(self, item):
return self.vars[item]
def is_valid(self):
for field in self.iter_fields():
if not field.is_valid()[0]:
return False
return True
def is_fake_form(self):
return bool(self.vars.get('fakeform', False))
class DataForm(ExtendedNode):
def __init__(self, type_=None, title=None, instructions=None, extend=None):
if extend is None:
# we have to build form from scratch
Node.__init__(self, 'x', attrs={'xmlns': Namespace.DATA})
if type_ is not None:
self.type_ = type_
if title is not None:
self.title = title
if instructions is not None:
self.instructions = instructions
@property
def type_(self):
"""
Type of the form. Must be one of: 'form', 'submit', 'cancel', 'result'.
'form' - this form is to be filled in; you will be able soon to do:
filledform = DataForm(replyto=thisform)
"""
return self.getAttr('type')
@type_.setter
def type_(self, type_):
assert type_ in ('form', 'submit', 'cancel', 'result')
self.setAttr('type', type_)
@property
def title(self):
"""
Title of the form
Human-readable, should not contain any \\r\\n.
"""
return self.getTagData('title')
@title.setter
def title(self, title):
self.setTagData('title', title)
@title.deleter
def title(self):
try:
self.delChild('title')
except ValueError:
pass
@property
def instructions(self):
"""
Instructions for this form
Human-readable, may contain \\r\\n.
"""
# TODO: the same code is in TextMultiField. join them
value = ''
for valuenode in self.getTags('instructions'):
value += '\n' + valuenode.getData()
return value[1:]
@instructions.setter
def instructions(self, value):
del self.instructions
if value == '':
return
for line in value.split('\n'):
self.addChild('instructions').setData(line)
@instructions.deleter
def instructions(self):
for value in self.getTags('instructions'):
self.delChild(value)
@property
def is_reported(self):
return self.getTag('reported') is not None
class SimpleDataForm(DataForm, DataRecord):
def __init__(self, type_=None, title=None, instructions=None, fields=None,
extend=None):
DataForm.__init__(self, type_=type_, title=title,
instructions=instructions, extend=extend)
DataRecord.__init__(self, fields=fields, extend=self, associated=self)
def get_purged(self):
simple_form = SimpleDataForm(extend=self)
del simple_form.title
simple_form.instructions = ''
to_be_removed = []
for field in simple_form.iter_fields():
if field.required:
# add if there is not
if hasattr(field, 'value') and not field.value:
field.value = ''
# Keep all required fields
continue
if ((hasattr(field, 'value') and
not field.value and
field.value != 0) or
(hasattr(field, 'values') and not field.values)):
to_be_removed.append(field)
else:
del field.label
del field.description
del field.media
for field in to_be_removed:
simple_form.delChild(field)
return simple_form
class MultipleDataForm(DataForm):
def __init__(self, type_=None, title=None, instructions=None, items=None,
extend=None):
DataForm.__init__(self, type_=type_, title=title,
instructions=instructions, extend=extend)
# all records, recorded into DataRecords
if extend is None:
if items is not None:
self.items = items
else:
# we already have nbxmpp.Node inside - try to convert all
# fields into DataField objects
if items is None:
self.items = list(self.iterTags('item'))
else:
for item in self.getTags('item'):
self.delChild(item)
self.items = items
reported_tag = self.getTag('reported')
self.reported = DataRecord(extend=reported_tag)
@property
def items(self):
"""
A list of all records
"""
return list(self.iter_records())
@items.setter
def items(self, records):
del self.items
for record in records:
if not isinstance(record, DataRecord):
DataRecord(extend=record)
self.addChild(node=record)
@items.deleter
def items(self):
for record in self.getTags('item'):
self.delChild(record)
def iter_records(self):
for record in self.getTags('item'):
yield record
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/date_and_time.py 0000664 0000000 0000000 00000013366 14130341156 0023754 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import re
import time
import logging
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from datetime import tzinfo
log = logging.getLogger('nbxmpp.m.date_and_time')
PATTERN_DATETIME = re.compile(
r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
r'T'
r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
r'(\.[0-9]{0,6})?'
r'(?:[0-9]+)?'
r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$'
)
PATTERN_DELAY = re.compile(
r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
r'T'
r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
r'(\.[0-9]{0,6})?'
r'(?:[0-9]+)?'
r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$'
)
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
SECOND = timedelta(seconds=1)
STDOFFSET = timedelta(seconds=-time.timezone)
if time.daylight:
DSTOFFSET = timedelta(seconds=-time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
class LocalTimezone(tzinfo):
'''
A class capturing the platform's idea of local time.
May result in wrong values on historical times in
timezones where UTC offset and/or the DST rules had
changed in the past.
'''
def fromutc(self, dt):
assert dt.tzinfo is self
stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND
args = time.localtime(stamp)[:6]
dst_diff = DSTDIFF // SECOND
# Detect fold
fold = (args == time.localtime(stamp - dst_diff))
return datetime(*args, microsecond=dt.microsecond,
tzinfo=self, fold=fold)
def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
return STDOFFSET
def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
return ZERO
def tzname(self, dt):
return 'local'
@staticmethod
def _isdst(dt):
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, 0)
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
def create_tzinfo(hours=0, minutes=0, tz_string=None):
if tz_string is None:
return timezone(timedelta(hours=hours, minutes=minutes))
if tz_string.lower() == 'z':
return timezone.utc
try:
hours, minutes = map(int, tz_string.split(':'))
except Exception:
log.warning('Wrong tz string: %s', tz_string)
return None
if hours not in range(-24, 24):
log.warning('Wrong tz string: %s', tz_string)
return None
if minutes not in range(0, 59):
log.warning('Wrong tz string: %s', tz_string)
return None
if hours in (24, -24) and minutes != 0:
log.warning('Wrong tz string: %s', tz_string)
return None
return timezone(timedelta(hours=hours, minutes=minutes))
def parse_datetime(timestring, check_utc=False,
convert='utc', epoch=False):
'''
Parse a XEP-0082 DateTime Profile String
:param timestring: a XEP-0082 DateTime profile formated string
:param check_utc: if True, returns None if timestring is not
a timestring expressing UTC
:param convert: convert the given timestring to utc or local time
:param epoch: if True, returns the time in epoch
Examples:
'2017-11-05T01:41:20Z'
'2017-11-05T01:41:20.123Z'
'2017-11-05T01:41:20.123+05:00'
return a datetime or epoch
'''
if timestring is None:
return None
if convert not in (None, 'utc', 'local'):
raise TypeError('"%s" is not a valid value for convert')
if check_utc:
match = PATTERN_DELAY.match(timestring)
else:
match = PATTERN_DATETIME.match(timestring)
if match:
timestring = ''.join(match.groups(''))
strformat = '%Y-%m-%d%H:%M:%S%z'
if match.group(3):
# Fractional second addendum to Time
strformat = '%Y-%m-%d%H:%M:%S.%f%z'
if match.group(4):
# UTC string denoted by addition of the character 'Z'
timestring = timestring[:-1] + '+0000'
try:
date_time = datetime.strptime(timestring, strformat)
except ValueError:
pass
else:
if check_utc:
if convert != 'utc':
raise ValueError(
'check_utc can only be used with convert="utc"')
date_time.replace(tzinfo=timezone.utc)
if epoch:
return date_time.timestamp()
return date_time
if convert == 'utc':
date_time = date_time.astimezone(timezone.utc)
if epoch:
return date_time.timestamp()
return date_time
if epoch:
# epoch is always UTC, use convert='utc' or check_utc=True
raise ValueError(
'epoch not available while converting to local')
if convert == 'local':
date_time = date_time.astimezone(LocalTimezone())
return date_time
# convert=None
return date_time
return None
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/delay.py 0000664 0000000 0000000 00000007664 14130341156 0022301 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.date_and_time import parse_datetime
from nbxmpp.modules.base import BaseModule
log = logging.getLogger('nbxmpp.m.delay')
class Delay(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_delay,
ns=Namespace.DELAY2,
priority=15),
StanzaHandler(name='presence',
callback=self._process_presence_delay,
ns=Namespace.DELAY2,
priority=15)
]
def _process_message_delay(self, _client, stanza, properties):
if properties.is_muc_subject:
# MUC Subjects can have a delay timestamp
# to indicate when the user has set the subject,
# the 'from' attr on these delays is the MUC server
# but we treat it as user timestamp
jids = [properties.jid.bare,
properties.jid.domain]
properties.user_timestamp = parse_delay(stanza, from_=jids)
else:
if properties.from_muc:
# Some servers use the MUC JID, others the domain
jids = [properties.jid.bare,
properties.jid.domain]
else:
jids = [self._client.get_bound_jid().domain]
server_delay = parse_delay(stanza, from_=jids)
if server_delay is not None:
properties.has_server_delay = True
properties.timestamp = server_delay
properties.user_timestamp = parse_delay(stanza, not_from=jids)
@staticmethod
def _process_presence_delay(_client, stanza, properties):
properties.user_timestamp = parse_delay(stanza)
def parse_delay(stanza, epoch=True, convert='utc', from_=None, not_from=None):
'''
Returns the first valid delay timestamp that matches
:param epoch: Returns the timestamp as epoch
:param convert: Converts the timestamp to either utc or local
:param from_: Matches only delays that have the according
from attr set
:param not_from: Matches only delays that have the according
from attr not set
'''
delays = stanza.getTags('delay', namespace=Namespace.DELAY2)
for delay in delays:
stamp = delay.getAttr('stamp')
if stamp is None:
log.warning('Invalid timestamp received: %s', stamp)
log.warning(stanza)
continue
delay_from = delay.getAttr('from')
if from_ is not None:
if delay_from not in from_:
continue
if not_from is not None:
if delay_from in not_from:
continue
timestamp = parse_datetime(stamp, check_utc=True,
epoch=epoch, convert=convert)
if timestamp is None:
log.warning('Invalid timestamp received: %s', stamp)
log.warning(stanza)
continue
return timestamp
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/delimiter.py 0000664 0000000 0000000 00000003674 14130341156 0023156 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Node
from nbxmpp.errors import StanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import process_response
class Delimiter(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def request_delimiter(self):
_task = yield
response = yield _make_request()
if response.isError():
raise StanzaError(response)
delimiter = response.getQuery().getTagData('roster') or None
yield delimiter
@iq_request_task
def set_delimiter(self, delimiter):
_task = yield
response = yield _make_set_request(delimiter)
yield process_response(response)
def _make_request():
node = Node('storage', attrs={'xmlns': Namespace.DELIMITER})
iq = Iq('get', Namespace.PRIVATE, payload=node)
return iq
def _make_set_request(delimiter):
iq = Iq('set', Namespace.PRIVATE)
roster = iq.getQuery().addChild('roster', namespace=Namespace.DELIMITER)
roster.setData(delimiter)
return iq
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/discovery.py 0000664 0000000 0000000 00000010760 14130341156 0023201 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import time
import logging
from nbxmpp.protocol import Iq
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import ErrorNode
from nbxmpp.protocol import ERR_ITEM_NOT_FOUND
from nbxmpp.protocol import NodeProcessed
from nbxmpp.modules.dataforms import extend_form
from nbxmpp.modules.base import BaseModule
from nbxmpp.structs import DiscoIdentity
from nbxmpp.structs import DiscoInfo
from nbxmpp.structs import DiscoItems
from nbxmpp.structs import DiscoItem
from nbxmpp.structs import StanzaHandler
from nbxmpp.task import iq_request_task
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import StanzaError
log = logging.getLogger('nbxmpp.m.discovery')
class Discovery(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='iq',
callback=self._process_disco_info,
typ='get',
ns=Namespace.DISCO_INFO,
priority=90),
]
@staticmethod
def _process_disco_info(client, stanza, properties):
iq = stanza.buildReply('error')
iq.addChild(node=ErrorNode(ERR_ITEM_NOT_FOUND))
client.send_stanza(iq)
raise NodeProcessed
@iq_request_task
def disco_info(self, jid, node=None):
_task = yield
self._log.info('Disco info: %s, node: %s', jid, node)
response = yield get_disco_request(Namespace.DISCO_INFO, jid, node)
if response.isError():
raise StanzaError(response)
yield parse_disco_info(response)
@iq_request_task
def disco_items(self, jid, node=None):
_task = yield
self._log.info('Disco items: %s, node: %s', jid, node)
response = yield get_disco_request(Namespace.DISCO_ITEMS, jid, node)
if response.isError():
raise StanzaError(response)
yield parse_disco_items(response)
def parse_disco_info(stanza, timestamp=None):
idenities = []
features = []
dataforms = []
if timestamp is None:
timestamp = time.time()
query = stanza.getQuery()
for node in query.getTags('identity'):
attrs = node.getAttrs()
try:
idenities.append(
DiscoIdentity(category=attrs['category'],
type=attrs['type'],
name=attrs.get('name'),
lang=attrs.get('xml:lang')))
except Exception:
raise MalformedStanzaError('invalid attributes', stanza)
for node in query.getTags('feature'):
try:
features.append(node.getAttr('var'))
except Exception:
raise MalformedStanzaError('invalid attributes', stanza)
for node in query.getTags('x', namespace=Namespace.DATA):
dataforms.append(extend_form(node))
return DiscoInfo(stanza=stanza,
identities=idenities,
features=features,
dataforms=dataforms,
timestamp=timestamp)
def parse_disco_items(stanza):
items = []
query = stanza.getQuery()
for node in query.getTags('item'):
attrs = node.getAttrs()
try:
items.append(
DiscoItem(jid=attrs['jid'],
name=attrs.get('name'),
node=attrs.get('node')))
except Exception:
raise MalformedStanzaError('invalid attributes', stanza)
return DiscoItems(jid=stanza.getFrom(),
node=query.getAttr('node'),
items=items)
def get_disco_request(namespace, jid, node=None):
iq = Iq('get', to=jid, queryNS=namespace)
if node:
iq.getQuery().setAttr('node', node)
return iq
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/eme.py 0000664 0000000 0000000 00000003314 14130341156 0021735 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import EMEData
from nbxmpp.modules.base import BaseModule
class EME(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_eme,
ns=Namespace.EME,
priority=40)
]
def _process_eme(self, _client, stanza, properties):
encryption = stanza.getTag('encryption', namespace=Namespace.EME)
if encryption is None:
return
name = encryption.getAttr('name')
namespace = encryption.getAttr('namespace')
if namespace is None:
self._log.warning('No namespace on message')
return
properties.eme = EMEData(name=name, namespace=namespace)
self._log.info('Found data: %s', properties.eme)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/entity_caps.py 0000664 0000000 0000000 00000007375 14130341156 0023524 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import EntityCapsData
from nbxmpp.structs import DiscoInfo
from nbxmpp.util import compute_caps_hash
from nbxmpp.modules.base import BaseModule
class EntityCaps(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='presence',
callback=self._process_entity_caps,
ns=Namespace.CAPS,
priority=15),
StanzaHandler(name='iq',
callback=self._process_disco_info,
typ='get',
ns=Namespace.DISCO_INFO,
priority=20),
]
self._identities = []
self._features = []
self._uri = None
self._node = None
self._caps = None
self._caps_hash = None
def _process_disco_info(self, client, stanza, _properties):
if self._caps is None:
return
node = stanza.getQuerynode()
if node is not None:
if self._node != node:
return
iq = stanza.buildReply('result')
if node is not None:
iq.setQuerynode(node)
query = iq.getQuery()
for identity in self._caps.identities:
query.addChild(node=identity.get_node())
for feature in self._caps.features:
query.addChild('feature', attrs={'var': feature})
self._log.info('Respond with disco info')
client.send_stanza(iq)
raise NodeProcessed
def _process_entity_caps(self, _client, stanza, properties):
caps = stanza.getTag('c', namespace=Namespace.CAPS)
if caps is None:
return
hash_algo = caps.getAttr('hash')
if hash_algo != 'sha-1':
self._log.warning('Unsupported hashing algorithm used: %s',
hash_algo)
self._log.warning(stanza)
return
node = caps.getAttr('node')
if not node:
self._log.warning('node attribute missing')
self._log.warning(stanza)
return
ver = caps.getAttr('ver')
if not ver:
self._log.warning('ver attribute missing')
self._log.warning(stanza)
return
properties.entity_caps = EntityCapsData(
hash=hash_algo,
node=node,
ver=ver)
@property
def caps(self):
if self._caps is None:
return None
return EntityCapsData(hash='sha-1',
node=self._uri,
ver=self._caps_hash)
def set_caps(self, identities, features, uri):
self._uri = uri
self._caps = DiscoInfo(None, identities, features, [])
self._caps_hash = compute_caps_hash(self._caps, compare=False)
self._node = '%s#%s' % (uri, self._caps_hash)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/http_auth.py 0000664 0000000 0000000 00000003733 14130341156 0023174 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import HTTPAuthData
from nbxmpp.modules.base import BaseModule
class HTTPAuth(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_http_auth,
ns=Namespace.HTTP_AUTH,
priority=40),
StanzaHandler(name='iq',
callback=self._process_http_auth,
typ='get',
ns=Namespace.HTTP_AUTH,
priority=40)
]
def _process_http_auth(self, _client, stanza, properties):
confirm = stanza.getTag('confirm', namespace=Namespace.HTTP_AUTH)
if confirm is None:
return
attrs = confirm.getAttrs()
body = stanza.getTagData('body')
id_ = attrs.get('id')
method = attrs.get('method')
url = attrs.get('url')
properties.http_auth = HTTPAuthData(id_, method, url, body)
self._log.info('HTTPAuth received: %s %s %s %s',
id_, method, url, body)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/http_upload.py 0000664 0000000 0000000 00000005523 14130341156 0023516 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.structs import HTTPUploadData
from nbxmpp.errors import HTTPUploadStanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.base import BaseModule
ALLOWED_HEADERS = ['Authorization', 'Cookie', 'Expires']
class HTTPUpload(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def request_slot(self, jid, filename, size, content_type):
_task = yield
response = yield _make_request(jid, filename, size, content_type)
if response.isError():
raise HTTPUploadStanzaError(response)
slot = response.getTag('slot', namespace=Namespace.HTTPUPLOAD_0)
if slot is None:
raise MalformedStanzaError('slot node missing', response)
put_uri = slot.getTagAttr('put', 'url')
if put_uri is None:
raise MalformedStanzaError('put uri missing', response)
get_uri = slot.getTagAttr('get', 'url')
if get_uri is None:
raise MalformedStanzaError('get uri missing', response)
headers = {}
for header in slot.getTag('put').getTags('header'):
name = header.getAttr('name')
if name not in ALLOWED_HEADERS:
raise MalformedStanzaError(
'not allowed header found: %s' % name, response)
data = header.getData()
if '\n' in data:
raise MalformedStanzaError(
'newline in header data found', response)
headers[name] = data
yield HTTPUploadData(put_uri=put_uri,
get_uri=get_uri,
headers=headers)
def _make_request(jid, filename, size, content_type):
iq = Iq(typ='get', to=jid)
attr = {'filename': filename,
'size': size,
'content-type': content_type}
iq.setTag(name="request",
namespace=Namespace.HTTPUPLOAD_0,
attrs=attr)
return iq
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/ibb.py 0000664 0000000 0000000 00000013501 14130341156 0021722 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.protocol import Error as ErrorStanza
from nbxmpp.protocol import ERR_BAD_REQUEST
from nbxmpp.protocol import ERR_FEATURE_NOT_IMPLEMENTED
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import Iq
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import IBBData
from nbxmpp.util import b64decode
from nbxmpp.util import b64encode
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import process_response
from nbxmpp.task import iq_request_task
class IBB(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='iq',
callback=self._process_ibb,
ns=Namespace.IBB,
priority=20),
]
def _process_ibb(self, _client, stanza, properties):
if properties.type.is_set:
open_ = stanza.getTag('open', namespace=Namespace.IBB)
if open_ is not None:
properties.ibb = self._parse_open(stanza, open_)
return
close = stanza.getTag('close', namespace=Namespace.IBB)
if close is not None:
properties.ibb = self._parse_close(stanza, close)
return
data = stanza.getTag('data', namespace=Namespace.IBB)
if data is not None:
properties.ibb = self._parse_data(stanza, data)
return
def _parse_open(self, stanza, open_):
attrs = open_.getAttrs()
try:
block_size = int(attrs.get('block-size'))
except Exception as error:
self._log.warning(error)
self._log.warning(stanza)
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
if block_size > 65535:
self._log.warning('Invalid block-size')
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
sid = attrs.get('sid')
if not sid:
self._log.warning('Invalid sid')
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
type_ = attrs.get('stanza')
if type_ == 'message':
self._client.send_stanza(ErrorStanza(stanza,
ERR_FEATURE_NOT_IMPLEMENTED))
raise NodeProcessed
return IBBData(type='open', block_size=block_size, sid=sid)
def _parse_close(self, stanza, close):
sid = close.getAttrs().get('sid')
if sid is None:
self._log.warning('Invalid sid')
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
return IBBData(type='close', sid=sid)
def _parse_data(self, stanza, data):
attrs = data.getAttrs()
sid = attrs.get('sid')
if sid is None:
self._log.warning('Invalid sid')
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
try:
seq = int(attrs.get('seq'))
except Exception:
self._log.exception('Invalid seq')
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
try:
decoded_data = b64decode(data.getData(), return_type=bytes)
except Exception:
self._log.exception('Failed to decode IBB data')
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
return IBBData(type='data', sid=sid, seq=seq, data=decoded_data)
def send_reply(self, stanza, error=None):
if error is None:
reply = stanza.buildReply('result')
reply.getChildren().clear()
else:
reply = ErrorStanza(stanza, error)
self._client.send_stanza(reply)
@iq_request_task
def send_open(self, jid, sid, block_size):
_task = yield
response = yield _make_ibb_open(jid, sid, block_size)
yield process_response(response)
@iq_request_task
def send_close(self, jid, sid):
_task = yield
response = yield _make_ibb_close(jid, sid)
yield process_response(response)
@iq_request_task
def send_data(self, jid, sid, seq, data):
_task = yield
response = yield _make_ibb_data(jid, sid, seq, data)
yield process_response(response)
def _make_ibb_open(jid, sid, block_size):
iq = Iq('set', to=jid)
iq.addChild('open',
{'block-size': block_size, 'sid': sid, 'stanza': 'iq'},
namespace=Namespace.IBB)
return iq
def _make_ibb_close(jid, sid):
iq = Iq('set', to=jid)
iq.addChild('close', {'sid': sid}, namespace=Namespace.IBB)
return iq
def _make_ibb_data(jid, sid, seq, data):
iq = Iq('set', to=jid)
ibb_data = iq.addChild('data',
{'sid': sid, 'seq': seq},
namespace=Namespace.IBB)
ibb_data.setData(b64encode(data))
return iq
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/idle.py 0000664 0000000 0000000 00000003523 14130341156 0022106 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.date_and_time import parse_datetime
from nbxmpp.modules.base import BaseModule
class Idle(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='presence',
callback=self._process_idle,
ns=Namespace.IDLE,
priority=15)
]
def _process_idle(self, _client, stanza, properties):
idle_tag = stanza.getTag('idle', namespace=Namespace.IDLE)
if idle_tag is None:
return
since = idle_tag.getAttr('since')
if since is None:
self._log.warning('No since attr in idle node')
self._log.warning(stanza)
return
timestamp = parse_datetime(since, convert='utc', epoch=True)
if timestamp is None:
self._log.warning('Invalid timestamp received: %s', since)
self._log.warning(stanza)
properties.idle_timestamp = timestamp
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/iq.py 0000664 0000000 0000000 00000004065 14130341156 0021604 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.protocol import Error as ErrorStanza
from nbxmpp.protocol import ERR_BAD_REQUEST
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.util import error_factory
from nbxmpp.const import IqType
from nbxmpp.modules.base import BaseModule
class BaseIq(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='iq',
callback=self._process_iq_base,
priority=10),
]
def _process_iq_base(self, _client, stanza, properties):
try:
properties.type = IqType(stanza.getType())
except ValueError:
self._log.warning('Message with invalid type: %s', stanza.getType())
self._log.warning(stanza)
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
properties.jid = stanza.getFrom()
properties.id = stanza.getID()
childs = stanza.getChildren()
for child in childs:
if child.getName() != 'error':
properties.payload = child
break
properties.query = stanza.getQuery()
if properties.type.is_error:
properties.error = error_factory(stanza)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/location.py 0000664 0000000 0000000 00000005510 14130341156 0022777 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import LocationData
from nbxmpp.const import LOCATION_DATA
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
from nbxmpp.task import iq_request_task
class Location(BaseModule):
_depends = {
'publish': 'PubSub'
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_location,
ns=Namespace.PUBSUB_EVENT,
priority=16),
]
def _process_pubsub_location(self, _client, _stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.LOCATION:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
location_node = item.getTag('geoloc', namespace=Namespace.LOCATION)
if not location_node.getChildren():
self._log.info('Received location: %s - no location set',
properties.jid)
return
location_dict = {}
for node in LOCATION_DATA:
location_dict[node] = location_node.getTagData(node)
data = LocationData(**location_dict)
pubsub_event = properties.pubsub_event._replace(data=data)
self._log.info('Received location: %s - %s', properties.jid, data)
properties.pubsub_event = pubsub_event
@iq_request_task
def set_location(self, data):
task = yield
item = Node('geoloc', {'xmlns': Namespace.LOCATION})
if data is not None:
data = data._asdict()
for tag, value in data.items():
if value is not None:
item.addChild(tag, payload=value)
result = yield self.publish(Namespace.LOCATION, item, id_='current')
yield finalize(task, result)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/mam.py 0000664 0000000 0000000 00000014345 14130341156 0021747 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.protocol import JID
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import MAMQueryData
from nbxmpp.structs import MAMPreferencesData
from nbxmpp.task import iq_request_task
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.rsm import parse_rsm
from nbxmpp.modules.dataforms import SimpleDataForm
from nbxmpp.modules.dataforms import create_field
from nbxmpp.modules.util import process_response
class MAM(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def make_query(self,
jid,
queryid=None,
start=None,
end=None,
with_=None,
after=None,
max_=70):
_task = yield
response = yield _make_request(jid, queryid,
start, end, with_, after, max_)
if response.isError():
raise StanzaError(response)
jid = response.getFrom()
fin = response.getTag('fin', namespace=Namespace.MAM_2)
if fin is None:
raise MalformedStanzaError('fin node missing', response)
rsm = parse_rsm(fin)
if rsm is None:
raise MalformedStanzaError('rsm set missing', response)
complete = fin.getAttr('complete') == 'true'
if max_ != 0 and not complete:
# max_ == 0 is a request for count of the items in a result set
# in this case first and last will be absent
# See: https://xmpp.org/extensions/xep-0059.html#count
if rsm.first is None or rsm.last is None:
raise MalformedStanzaError('first or last element missing',
response)
yield MAMQueryData(jid=jid,
complete=complete,
rsm=rsm)
@iq_request_task
def request_preferences(self):
_task = yield
response = yield _make_pref_request()
if response.isError():
raise StanzaError(response)
prefs = response.getTag('prefs', namespace=Namespace.MAM_2)
if prefs is None:
raise MalformedStanzaError('prefs node missing', response)
default = prefs.getAttr('default')
if default is None:
raise MalformedStanzaError('default attr missing', response)
always_node = prefs.getTag('always')
if always_node is None:
raise MalformedStanzaError('always node missing', response)
always = _get_preference_jids(always_node)
never_node = prefs.getTag('never')
if never_node is None:
raise MalformedStanzaError('never node missing', response)
never = _get_preference_jids(never_node)
yield MAMPreferencesData(default=default,
always=always,
never=never)
@iq_request_task
def set_preferences(self, default, always, never):
_task = yield
if default not in ('always', 'never', 'roster'):
raise ValueError('Wrong default preferences type')
response = yield _make_set_pref_request(default, always, never)
yield process_response(response)
def _make_query_form(start, end, with_):
fields = [
create_field(typ='hidden', var='FORM_TYPE', value=Namespace.MAM_2)
]
if start:
fields.append(create_field(
typ='text-single',
var='start',
value=start.strftime('%Y-%m-%dT%H:%M:%SZ')))
if end:
fields.append(create_field(
typ='text-single',
var='end',
value=end.strftime('%Y-%m-%dT%H:%M:%SZ')))
if with_:
fields.append(create_field(
typ='jid-single',
var='with',
value=with_))
return SimpleDataForm(type_='submit', fields=fields)
def _make_rsm_query(max_, after):
rsm_set = Node('set', attrs={'xmlns': Namespace.RSM})
if max_ is not None:
rsm_set.setTagData('max', max_)
if after is not None:
rsm_set.setTagData('after', after)
return rsm_set
def _make_request(jid, queryid, start, end, with_, after, max_):
iq = Iq(typ='set', to=jid, queryNS=Namespace.MAM_2)
if queryid is not None:
iq.getQuery().setAttr('queryid', queryid)
payload = [
_make_query_form(start, end, with_),
_make_rsm_query(max_, after)
]
iq.setQueryPayload(payload)
return iq
def _make_pref_request():
iq = Iq('get', queryNS=Namespace.MAM_2)
iq.setQuery('prefs')
return iq
def _get_preference_jids(node):
jids = []
for item in node.getTags('jid'):
jid = item.getData()
if not jid:
continue
try:
jid = JID.from_string(jid)
except Exception:
continue
jids.append(jid)
return jids
def _make_set_pref_request(default, always, never):
iq = Iq(typ='set')
prefs = iq.addChild(name='prefs',
namespace=Namespace.MAM_2,
attrs={'default': default})
always_node = prefs.addChild(name='always')
never_node = prefs.addChild(name='never')
for jid in always:
always_node.addChild(name='jid').setData(jid)
for jid in never:
never_node.addChild(name='jid').setData(jid)
return iq
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/message.py 0000664 0000000 0000000 00000007463 14130341156 0022624 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.protocol import NodeProcessed
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import StanzaIDData
from nbxmpp.structs import XHTMLData
from nbxmpp.util import error_factory
from nbxmpp.const import MessageType
from nbxmpp.modules.base import BaseModule
class BaseMessage(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_base,
priority=5),
StanzaHandler(name='message',
callback=self._process_message_after_base,
priority=10),
]
def _process_message_base(self, _client, stanza, properties):
properties.type = self._parse_type(stanza)
# Determine remote JID
if properties.is_carbon_message and properties.carbon.is_sent:
properties.jid = stanza.getTo()
elif properties.is_mam_message and not properties.type.is_groupchat:
own_jid = self._client.get_bound_jid()
if own_jid.bare_match(stanza.getFrom()):
properties.jid = stanza.getTo()
else:
properties.jid = stanza.getFrom()
else:
properties.jid = stanza.getFrom()
properties.from_ = stanza.getFrom()
properties.to = stanza.getTo()
properties.id = stanza.getID()
properties.self_message = self._parse_self_message(stanza, properties)
# Stanza ID
id_, by = stanza.getStanzaIDAttrs()
if id_ is not None and by is not None:
properties.stanza_id = StanzaIDData(id=id_, by=by)
if properties.type.is_error:
properties.error = error_factory(stanza)
def _process_message_after_base(self, _client, stanza, properties):
# This handler runs after decryption handlers had the chance
# to decrypt the body
properties.body = stanza.getBody()
properties.thread = stanza.getThread()
properties.subject = stanza.getSubject()
forms = stanza.getTags('x', namespace=Namespace.DATA)
if forms:
properties.forms = forms
xhtml = stanza.getXHTML()
if xhtml is None:
return
if xhtml.getTag('body', namespace=Namespace.XHTML) is None:
self._log.warning('xhtml without body found')
self._log.warning(stanza)
return
properties.xhtml = XHTMLData(xhtml)
def _parse_type(self, stanza):
type_ = stanza.getType()
if type_ is None:
return MessageType.NORMAL
try:
return MessageType(type_)
except ValueError:
self._log.warning('Message with invalid type: %s', type_)
self._log.warning(stanza)
raise NodeProcessed
@staticmethod
def _parse_self_message(stanza, properties):
if properties.type.is_groupchat:
return False
return stanza.getFrom().bare_match(stanza.getTo())
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/misc.py 0000664 0000000 0000000 00000010374 14130341156 0022126 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import InvalidFrom
from nbxmpp.protocol import InvalidStanza
from nbxmpp.protocol import Message
from nbxmpp.structs import MAMData
from nbxmpp.structs import CarbonData
from nbxmpp.modules.delay import parse_delay
log = logging.getLogger('nbxmpp.m.misc')
def unwrap_carbon(stanza, own_jid):
carbon = stanza.getTag('received', namespace=Namespace.CARBONS)
if carbon is None:
carbon = stanza.getTag('sent', namespace=Namespace.CARBONS)
if carbon is None:
return stanza, None
# Carbon must be from our bare jid
if stanza.getFrom() != own_jid.new_as_bare():
raise InvalidFrom('Invalid from: %s' % stanza.getAttr('from'))
forwarded = carbon.getTag('forwarded', namespace=Namespace.FORWARD)
message = Message(node=forwarded.getTag('message'))
type_ = carbon.getName()
# Fill missing to/from
to = message.getTo()
if to is None:
message.setTo(own_jid.bare)
frm = message.getFrom()
if frm is None:
message.setFrom(own_jid.bare)
if type_ == 'received':
if message.getFrom().bare_match(own_jid):
# Drop 'received' Carbons from ourself, we already
# got the message with the 'sent' Carbon or via the
# message itself
raise NodeProcessed('Drop "received"-Carbon from ourself')
if message.getTag('x', namespace=Namespace.MUC_USER) is not None:
# A MUC broadcasts messages sent to us to all resources
# there is no need to process the received carbon
raise NodeProcessed('Drop MUC-PM "received"-Carbon')
return message, CarbonData(type=type_)
def unwrap_mam(stanza, own_jid):
result = stanza.getTag('result', namespace=Namespace.MAM_2)
if result is None:
result = stanza.getTag('result', namespace=Namespace.MAM_1)
if result is None:
return stanza, None
query_id = result.getAttr('queryid')
if query_id is None:
log.warning('No queryid on MAM message')
log.warning(stanza)
raise InvalidStanza
id_ = result.getAttr('id')
if id_ is None:
log.warning('No id on MAM message')
log.warning(stanza)
raise InvalidStanza
forwarded = result.getTag('forwarded', namespace=Namespace.FORWARD)
message = Message(node=forwarded.getTag('message'))
# Fill missing to/from
to = message.getTo()
if to is None:
message.setTo(own_jid.bare)
frm = message.getFrom()
if frm is None:
message.setFrom(own_jid.bare)
# Timestamp parsing
# Most servers dont set the 'from' attr, so we cant check for it
delay_timestamp = parse_delay(forwarded)
if delay_timestamp is None:
log.warning('No timestamp on MAM message')
log.warning(stanza)
raise InvalidStanza
return message, MAMData(id=id_,
query_id=query_id,
archive=stanza.getFrom(),
namespace=result.getNamespace(),
timestamp=delay_timestamp)
def build_xhtml_body(xhtml, xmllang=None):
try:
if xmllang is not None:
body = '%s' % (
Namespace.XHTML, xmllang, xhtml)
else:
body = '%s' % (Namespace.XHTML, xhtml)
except Exception as error:
log.error('Error while building xhtml node: %s', error)
return None
return body
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/mood.py 0000664 0000000 0000000 00000006032 14130341156 0022125 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import MoodData
from nbxmpp.const import MOODS
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
from nbxmpp.task import iq_request_task
class Mood(BaseModule):
_depends = {
'publish': 'PubSub'
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_mood,
ns=Namespace.PUBSUB_EVENT,
priority=16),
]
def _process_pubsub_mood(self, _client, stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.MOOD:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
mood_node = item.getTag('mood', namespace=Namespace.MOOD)
if not mood_node.getChildren():
self._log.info('Received mood: %s - removed mood', properties.jid)
return
mood, text = None, None
for child in mood_node.getChildren():
name = child.getName().strip()
if name == 'text':
text = child.getData()
elif name in MOODS:
mood = name
if mood is None and mood_node.getPayload():
self._log.warning('No valid mood value found')
self._log.warning(stanza)
raise NodeProcessed
data = MoodData(mood, text)
pubsub_event = properties.pubsub_event._replace(data=data)
self._log.info('Received mood: %s - %s', properties.jid, data)
properties.pubsub_event = pubsub_event
@iq_request_task
def set_mood(self, data):
task = yield
item = Node('mood', {'xmlns': Namespace.MOOD})
if data is not None and data.mood:
item.addChild(data.mood)
if data.text:
item.addChild('text', payload=data.text)
result = yield self.publish(Namespace.MOOD, item, id_='current')
yield finalize(task, result)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/muc/ 0000775 0000000 0000000 00000000000 14130341156 0021400 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/muc/__init__.py 0000664 0000000 0000000 00000000025 14130341156 0023506 0 ustar 00root root 0000000 0000000 from .muc import MUC
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/muc/muc.py 0000664 0000000 0000000 00000045235 14130341156 0022547 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import ERR_NOT_ACCEPTABLE
from nbxmpp.protocol import JID
from nbxmpp.protocol import Message
from nbxmpp.protocol import DataForm
from nbxmpp.protocol import DataField
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import StanzaMalformed
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import InviteType
from nbxmpp.const import MessageType
from nbxmpp.const import StatusCode
from nbxmpp.structs import DeclineData
from nbxmpp.structs import InviteData
from nbxmpp.structs import VoiceRequest
from nbxmpp.structs import AffiliationResult
from nbxmpp.structs import MucConfigResult
from nbxmpp.structs import MucDestroyed
from nbxmpp.task import iq_request_task
from nbxmpp.errors import is_error
from nbxmpp.errors import StanzaError
from nbxmpp.modules.util import raise_if_error
from nbxmpp.modules.util import parse_xmpp_uri
from nbxmpp.modules.util import process_response
from nbxmpp.modules.dataforms import extend_form
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.muc.util import MucInfoResult
from nbxmpp.modules.muc.util import make_affiliation_request
from nbxmpp.modules.muc.util import make_destroy_request
from nbxmpp.modules.muc.util import make_set_config_request
from nbxmpp.modules.muc.util import make_config_request
from nbxmpp.modules.muc.util import make_cancel_config_request
from nbxmpp.modules.muc.util import make_set_affiliation_request
from nbxmpp.modules.muc.util import make_set_role_request
from nbxmpp.modules.muc.util import make_captcha_request
from nbxmpp.modules.muc.util import build_direct_invite
from nbxmpp.modules.muc.util import build_mediated_invite
from nbxmpp.modules.muc.util import parse_muc_user
class MUC(BaseModule):
_depends = {
'disco_info': 'Discovery',
'request_vcard': 'VCardTemp',
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='presence',
callback=self._process_muc_presence,
ns=Namespace.MUC,
priority=11),
StanzaHandler(name='presence',
callback=self._process_muc_user_presence,
ns=Namespace.MUC_USER,
priority=11),
StanzaHandler(name='message',
callback=self._process_groupchat_message,
typ='groupchat',
priority=6),
StanzaHandler(name='message',
callback=self._process_mediated_invite,
typ='normal',
ns=Namespace.MUC_USER,
priority=11),
StanzaHandler(name='message',
callback=self._process_direct_invite,
typ='normal',
ns=Namespace.CONFERENCE,
priority=12),
StanzaHandler(name='message',
callback=self._process_voice_request,
ns=Namespace.DATA,
priority=11),
StanzaHandler(name='message',
callback=self._process_message,
ns=Namespace.MUC_USER,
priority=13),
]
@staticmethod
def _process_muc_presence(_client, stanza, properties):
muc = stanza.getTag('x', namespace=Namespace.MUC)
if muc is None:
return
properties.from_muc = True
properties.muc_jid = properties.jid.new_as_bare()
properties.muc_nickname = properties.jid.resource
def _process_muc_user_presence(self, _client, stanza, properties):
muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
if muc_user is None:
return
properties.from_muc = True
properties.muc_jid = properties.jid.new_as_bare()
destroy = muc_user.getTag('destroy')
if destroy is not None:
alternate = destroy.getAttr('jid')
if alternate is not None:
try:
alternate = JID.from_string(alternate)
except Exception as error:
self._log.warning('Invalid alternate JID provided: %s',
error)
self._log.warning(stanza)
alternate = None
properties.muc_destroyed = MucDestroyed(
alternate=alternate,
reason=muc_user.getTagData('reason'),
password=muc_user.getTagData('password'))
return
properties.muc_nickname = properties.jid.resource
# https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes
message_status_codes = [
StatusCode.NON_ANONYMOUS,
StatusCode.SELF,
StatusCode.CONFIG_ROOM_LOGGING,
StatusCode.CREATED,
StatusCode.NICKNAME_MODIFIED,
StatusCode.REMOVED_BANNED,
StatusCode.NICKNAME_CHANGE,
StatusCode.REMOVED_KICKED,
StatusCode.REMOVED_AFFILIATION_CHANGE,
StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY,
StatusCode.REMOVED_SERVICE_SHUTDOWN,
StatusCode.REMOVED_ERROR,
]
codes = set()
for status in muc_user.getTags('status'):
try:
code = StatusCode(status.getAttr('code'))
except ValueError:
self._log.warning('Received invalid status code: %s',
status.getAttr('code'))
self._log.warning(stanza)
continue
if code in message_status_codes:
codes.add(code)
if codes:
properties.muc_status_codes = codes
try:
properties.muc_user = parse_muc_user(muc_user)
except StanzaMalformed as error:
self._log.warning(error)
self._log.warning(stanza)
raise NodeProcessed
if (properties.muc_user is not None and
properties.muc_user.role.is_none and
not properties.type.is_unavailable):
self._log.warning('Malformed Stanza')
self._log.warning(stanza)
raise NodeProcessed
def _process_groupchat_message(self, _client, stanza, properties):
properties.from_muc = True
properties.muc_jid = properties.jid.new_as_bare()
properties.muc_nickname = properties.jid.resource
muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
if muc_user is not None:
try:
properties.muc_user = parse_muc_user(muc_user,
is_presence=False)
except StanzaMalformed as error:
self._log.warning(error)
self._log.warning(stanza)
raise NodeProcessed
addresses = stanza.getTag('addresses', namespace=Namespace.ADDRESS)
if addresses is not None:
address = addresses.getTag('address', attrs={'type': 'ofrom'})
if address is not None:
properties.muc_ofrom = JID.from_string(address.getAttr('jid'))
def _process_message(self, _client, stanza, properties):
muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
if muc_user is None:
return
# MUC Private message
if (properties.type.is_chat or
properties.type.is_error and
not muc_user.getChildren()):
properties.muc_private_message = True
return
if properties.is_muc_invite_or_decline:
return
properties.from_muc = True
properties.muc_jid = properties.jid.new_as_bare()
if not properties.jid.is_bare:
return
# MUC Config change
# https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes
message_status_codes = [
StatusCode.SHOWING_UNAVAILABLE,
StatusCode.NOT_SHOWING_UNAVAILABLE,
StatusCode.CONFIG_NON_PRIVACY_RELATED,
StatusCode.CONFIG_ROOM_LOGGING,
StatusCode.CONFIG_NO_ROOM_LOGGING,
StatusCode.CONFIG_NON_ANONYMOUS,
StatusCode.CONFIG_SEMI_ANONYMOUS,
StatusCode.CONFIG_FULL_ANONYMOUS
]
codes = set()
for status in muc_user.getTags('status'):
try:
code = StatusCode(status.getAttr('code'))
except ValueError:
self._log.warning('Received invalid status code: %s',
status.getAttr('code'))
self._log.warning(stanza)
continue
if code in message_status_codes:
codes.add(code)
if codes:
properties.muc_status_codes = codes
@staticmethod
def _process_direct_invite(_client, stanza, properties):
direct = stanza.getTag('x', namespace=Namespace.CONFERENCE)
if direct is None:
return
if stanza.getTag('x', namespace=Namespace.MUC_USER) is not None:
# not a direct invite
# See https://xmpp.org/extensions/xep-0045.html#example-57
# read implementation notes
return
data = {}
data['muc'] = JID.from_string(direct.getAttr('jid'))
data['from_'] = properties.jid
data['reason'] = direct.getAttr('reason')
data['password'] = direct.getAttr('password')
data['continued'] = direct.getAttr('continue') == 'true'
data['thread'] = direct.getAttr('thread')
data['type'] = InviteType.DIRECT
properties.muc_invite = InviteData(**data)
@staticmethod
def _process_mediated_invite(_client, stanza, properties):
muc_user = stanza.getTag('x', namespace=Namespace.MUC_USER)
if muc_user is None:
return
if properties.type != MessageType.NORMAL:
return
properties.from_muc = True
properties.muc_jid = properties.jid.new_as_bare()
data = {}
invite = muc_user.getTag('invite')
if invite is not None:
data['muc'] = properties.jid.new_as_bare()
data['from_'] = JID.from_string(invite.getAttr('from'))
data['reason'] = invite.getTagData('reason')
data['password'] = muc_user.getTagData('password')
data['type'] = InviteType.MEDIATED
data['continued'] = False
data['thread'] = None
continue_ = invite.getTag('continue')
if continue_ is not None:
data['continued'] = True
data['thread'] = continue_.getAttr('thread')
properties.muc_invite = InviteData(**data)
return
decline = muc_user.getTag('decline')
if decline is not None:
data['muc'] = properties.jid.new_as_bare()
data['from_'] = JID.from_string(decline.getAttr('from'))
data['reason'] = decline.getTagData('reason')
properties.muc_decline = DeclineData(**data)
return
def _process_voice_request(self, _client, stanza, properties):
data_form = stanza.getTag('x', namespace=Namespace.DATA)
if data_form is None:
return
data_form = extend_form(data_form)
try:
if data_form['FORM_TYPE'].value != Namespace.MUC_REQUEST:
return
except KeyError:
return
nick = data_form['muc#roomnick'].value
try:
jid = JID.from_string(data_form['muc#jid'].value)
except Exception:
self._log.warning('Invalid JID on voice request')
self._log.warning(stanza)
raise NodeProcessed
properties.voice_request = VoiceRequest(jid=jid,
nick=nick,
form=data_form)
properties.from_muc = True
properties.muc_jid = properties.jid.new_as_bare()
def approve_voice_request(self, muc_jid, voice_request):
form = voice_request.form
form.type_ = 'submit'
form['muc#request_allow'].value = True
self._client.send_stanza(Message(to=muc_jid, payload=form))
@iq_request_task
def get_affiliation(self, jid, affiliation):
_task = yield
response = yield make_affiliation_request(jid, affiliation)
if response.isError():
raise StanzaError(response)
room_jid = response.getFrom()
query = response.getTag('query', namespace=Namespace.MUC_ADMIN)
items = query.getTags('item')
users_dict = {}
for item in items:
try:
jid = JID.from_string(item.getAttr('jid'))
except Exception as error:
self._log.warning('Invalid JID: %s, %s',
item.getAttr('jid'), error)
continue
users_dict[jid] = {}
if item.has_attr('nick'):
users_dict[jid]['nick'] = item.getAttr('nick')
if item.has_attr('role'):
users_dict[jid]['role'] = item.getAttr('role')
reason = item.getTagData('reason')
if reason:
users_dict[jid]['reason'] = reason
self._log.info('Affiliations received from %s: %s',
room_jid, users_dict)
yield AffiliationResult(jid=room_jid, users=users_dict)
@iq_request_task
def destroy(self, room_jid, reason=None, jid=None):
_task = yield
response = yield make_destroy_request(room_jid, reason, jid)
yield process_response(response)
@iq_request_task
def request_info(self, jid, request_vcard=True, allow_redirect=False):
_task = yield
redirected = False
disco_info = yield self.disco_info(jid)
if is_error(disco_info):
error_response = disco_info
if not allow_redirect:
raise error_response
if error_response.condition != 'gone':
raise error_response
try:
jid = parse_xmpp_uri(error_response.condition_data)[0]
except Exception:
raise error_response
redirected = True
disco_info = yield self.disco_info(jid)
raise_if_error(disco_info)
if not request_vcard or not disco_info.supports(Namespace.VCARD):
yield MucInfoResult(info=disco_info, redirected=redirected)
vcard = yield self.request_vcard(jid)
if is_error(vcard):
yield MucInfoResult(info=disco_info, redirected=redirected)
yield MucInfoResult(info=disco_info,
vcard=vcard,
redirected=redirected)
@iq_request_task
def set_config(self, room_jid, form):
_task = yield
response = yield make_set_config_request(room_jid, form)
yield process_response(response)
@iq_request_task
def request_config(self, room_jid):
task = yield
response = yield make_config_request(room_jid)
if response.isError():
raise StanzaError(response)
jid = response.getFrom()
payload = response.getQueryPayload()
for form in payload:
if form.getNamespace() == Namespace.DATA:
dataform = extend_form(node=form)
self._log.info('Config form received for %s', jid)
yield MucConfigResult(jid=jid, form=dataform)
yield MucConfigResult(jid=jid)
@iq_request_task
def cancel_config(self, room_jid):
_task = yield
response = yield make_cancel_config_request(room_jid)
yield process_response(response)
@iq_request_task
def set_affiliation(self, room_jid, users_dict):
_task = yield
response = yield make_set_affiliation_request(room_jid, users_dict)
yield process_response(response)
@iq_request_task
def set_role(self, room_jid, nick, role, reason=None):
_task = yield
response = yield make_set_role_request(room_jid, nick, role, reason)
yield process_response(response)
def set_subject(self, room_jid, subject):
message = Message(room_jid, typ='groupchat', subject=subject)
self._log.info('Set subject for %s', room_jid)
self._client.send_stanza(message)
def decline(self, room, to, reason=None):
message = Message(to=room)
muc_user = message.addChild('x', namespace=Namespace.MUC_USER)
decline = muc_user.addChild('decline', attrs={'to': to})
if reason:
decline.setTagData('reason', reason)
self._client.send_stanza(message)
def request_voice(self, room):
message = Message(to=room)
xdata = DataForm(typ='submit')
xdata.addChild(node=DataField(name='FORM_TYPE',
value=Namespace.MUC_REQUEST))
xdata.addChild(node=DataField(name='muc#role',
value='participant',
typ='text-single'))
message.addChild(node=xdata)
self._client.send_stanza(message)
def invite(self, room, to, password, reason=None, continue_=False,
type_=InviteType.MEDIATED):
if type_ == InviteType.DIRECT:
invite = build_direct_invite(
room, to, reason, password, continue_)
else:
invite = build_mediated_invite(
room, to, reason, password, continue_)
return self._client.send_stanza(invite)
@iq_request_task
def send_captcha(self, room_jid, form_node):
_task = yield
response = yield make_captcha_request(room_jid, form_node)
yield process_response(response)
def cancel_captcha(self, room_jid, message_id):
message = Message(typ='error', to=room_jid)
message.setID(message_id)
message.setError(ERR_NOT_ACCEPTABLE)
self._client.send_stanza(message)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/muc/util.py 0000664 0000000 0000000 00000013115 14130341156 0022730 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from dataclasses import dataclass
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.protocol import JID
from nbxmpp.protocol import InvalidJid
from nbxmpp.protocol import StanzaMalformed
from nbxmpp.protocol import Message
from nbxmpp.simplexml import Node
from nbxmpp.const import Affiliation
from nbxmpp.const import Role
from nbxmpp.structs import MucUserData
@dataclass
class MucInfoResult:
info: object
vcard: object = None
redirected: bool = False
def make_affiliation_request(jid, affiliation):
iq = Iq(typ='get', to=jid, queryNS=Namespace.MUC_ADMIN)
item = iq.setQuery().setTag('item')
item.setAttr('affiliation', affiliation)
return iq
def make_set_affiliation_request(room_jid, users_dict):
iq = Iq(typ='set', to=room_jid, queryNS=Namespace.MUC_ADMIN)
item = iq.setQuery()
for jid in users_dict:
affiliation = users_dict[jid].get('affiliation')
reason = users_dict[jid].get('reason')
nick = users_dict[jid].get('nick')
item_tag = item.addChild('item', {'jid': jid,
'affiliation': affiliation})
if reason is not None:
item_tag.setTagData('reason', reason)
if nick is not None:
item_tag.setAttr('nick', nick)
return iq
def make_destroy_request(room_jid, reason, jid):
iq = Iq(typ='set', queryNS=Namespace.MUC_OWNER, to=room_jid)
destroy = iq.setQuery().setTag('destroy')
if reason:
destroy.setTagData('reason', reason)
if jid:
destroy.setAttr('jid', jid)
return iq
def make_set_config_request(room_jid, form):
iq = Iq(typ='set', to=room_jid, queryNS=Namespace.MUC_OWNER)
query = iq.setQuery()
form.setAttr('type', 'submit')
query.addChild(node=form)
return iq
def make_config_request(room_jid):
iq = Iq(typ='get',
queryNS=Namespace.MUC_OWNER,
to=room_jid)
return iq
def make_cancel_config_request(room_jid):
cancel = Node(tag='x', attrs={'xmlns': Namespace.DATA,
'type': 'cancel'})
iq = Iq(typ='set',
queryNS=Namespace.MUC_OWNER,
payload=cancel,
to=room_jid)
return iq
def make_set_role_request(room_jid, nick, role, reason):
iq = Iq(typ='set', to=room_jid, queryNS=Namespace.MUC_ADMIN)
item = iq.setQuery().setTag('item')
item.setAttr('nick', nick)
item.setAttr('role', role)
if reason:
item.addChild(name='reason', payload=reason)
return iq
def make_captcha_request(room_jid, form_node):
iq = Iq(typ='set', to=room_jid)
captcha = iq.addChild(name='captcha', namespace=Namespace.CAPTCHA)
captcha.addChild(node=form_node)
return iq
def build_direct_invite(room, to, reason, password, continue_):
message = Message(to=to)
attrs = {'jid': room}
if reason:
attrs['reason'] = reason
if continue_:
attrs['continue'] = 'true'
if password:
attrs['password'] = password
message.addChild(name='x', attrs=attrs,
namespace=Namespace.CONFERENCE)
return message
def build_mediated_invite(room, to, reason, password, continue_):
message = Message(to=room)
muc_user = message.addChild('x', namespace=Namespace.MUC_USER)
invite = muc_user.addChild('invite', attrs={'to': to})
if continue_:
invite.addChild(name='continue')
if reason:
invite.setTagData('reason', reason)
if password:
muc_user.setTagData('password', password)
return message
def parse_muc_user(muc_user, is_presence=True):
item = muc_user.getTag('item')
if item is None:
return None
item_dict = item.getAttrs()
role = item_dict.get('role')
if role is not None:
try:
role = Role(role)
except ValueError:
raise StanzaMalformed('invalid role %s' % role)
elif is_presence:
# role attr MUST be included in all presence broadcasts
raise StanzaMalformed('role attr missing')
affiliation = item_dict.get('affiliation')
if affiliation is not None:
try:
affiliation = Affiliation(affiliation)
except ValueError:
raise StanzaMalformed('invalid affiliation %s' % affiliation)
elif is_presence:
# affiliation attr MUST be included in all presence broadcasts
raise StanzaMalformed('affiliation attr missing')
jid = item_dict.get('jid')
if jid is not None:
try:
jid = JID.from_string(jid)
except InvalidJid as error:
raise StanzaMalformed('invalid jid %s, %s' % (jid, error))
return MucUserData(affiliation=affiliation,
jid=jid,
nick=item.getAttr('nick'),
role=role,
actor=item.getTagAttr('actor', 'nick'),
reason=item.getTagData('reason'))
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/muclumbus.py 0000664 0000000 0000000 00000017017 14130341156 0023210 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import json
from gi.repository import Soup
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.protocol import Iq
from nbxmpp.structs import MuclumbusItem
from nbxmpp.structs import MuclumbusResult
from nbxmpp.const import AnonymityMode
from nbxmpp.modules.dataforms import extend_form
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.task import http_request_task
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
# API Documentation
# https://search.jabber.network/docs/api
class Muclumbus(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
self._proxy_resolver = None
self._soup_session = Soup.Session()
def set_proxy(self, proxy):
if proxy is None:
return
self._proxy_resolver = proxy.get_resolver()
self._soup_session.props.proxy_resolver = self._proxy_resolver
@iq_request_task
def request_parameters(self, jid):
task = yield
response = yield _make_parameter_request(jid)
if response.isError():
raise StanzaError(response)
search = response.getTag('search', namespace=Namespace.MUCLUMBUS)
if search is None:
raise MalformedStanzaError('search node missing', response)
dataform = search.getTag('x', namespace=Namespace.DATA)
if dataform is None:
raise MalformedStanzaError('dataform node missing', response)
self._log.info('Muclumbus parameters received')
yield finalize(task, extend_form(node=dataform))
@iq_request_task
def set_search(self, jid, dataform, items_per_page=50, after=None):
_task = yield
response = yield _make_search_query(jid,
dataform,
items_per_page,
after)
if response.isError():
raise StanzaError(response)
result = response.getTag('result', namespace=Namespace.MUCLUMBUS)
if result is None:
raise MalformedStanzaError('result node missing', response)
items = result.getTags('item')
if not items:
yield MuclumbusResult(first=None,
last=None,
max=None,
end=True,
items=[])
set_ = result.getTag('set', namespace=Namespace.RSM)
if set_ is None:
raise MalformedStanzaError('set node missing', response)
first = set_.getTagData('first')
last = set_.getTagData('last')
try:
max_ = int(set_.getTagData('max'))
except Exception:
raise MalformedStanzaError('invalid max value', response)
results = []
for item in items:
jid = item.getAttr('address')
name = item.getTagData('name')
nusers = item.getTagData('nusers')
description = item.getTagData('description')
language = item.getTagData('language')
is_open = item.getTag('is-open') is not None
try:
anonymity_mode = AnonymityMode(
item.getTagData('anonymity-mode'))
except ValueError:
anonymity_mode = AnonymityMode.UNKNOWN
results.append(MuclumbusItem(jid=jid,
name=name or '',
nusers=nusers or '',
description=description or '',
language=language or '',
is_open=is_open,
anonymity_mode=anonymity_mode))
yield MuclumbusResult(first=first,
last=last,
max=max_,
end=len(items) < max_,
items=results)
@http_request_task
def set_http_search(self, uri, keywords, after=None):
_task = yield
search = {'keywords': keywords}
if after is not None:
search['after'] = after
message = Soup.Message.new('POST', uri)
message.set_request('application/json',
Soup.MemoryUse.COPY,
json.dumps(search).encode())
response_message = yield message
soup_body = response_message.get_property('response-body')
if response_message.status_code != 200:
self._log.warning(soup_body.data)
yield MuclumbusResult(first=None,
last=None,
max=None,
end=True,
items=[])
response = json.loads(soup_body.data)
result = response['result']
items = result.get('items')
if items is None:
yield MuclumbusResult(first=None,
last=None,
max=None,
end=True,
items=[])
results = []
for item in items:
try:
anonymity_mode = AnonymityMode(item['anonymity_mode'])
except (ValueError, KeyError):
anonymity_mode = AnonymityMode.UNKNOWN
results.append(
MuclumbusItem(jid=item['address'],
name=item['name'] or '',
nusers=str(item['nusers'] or ''),
description=item['description'] or '',
language=item['language'] or '',
is_open=item['is_open'],
anonymity_mode=anonymity_mode))
yield MuclumbusResult(first=None,
last=result['last'],
max=None,
end=not result['more'],
items=results)
def _make_parameter_request(jid):
query = Iq(to=jid, typ='get')
query.addChild(node=Node('search',
attrs={'xmlns': Namespace.MUCLUMBUS}))
return query
def _make_search_query(jid, dataform, items_per_page=50, after=None):
search = Node('search', attrs={'xmlns': Namespace.MUCLUMBUS})
search.addChild(node=dataform)
rsm = search.addChild('set', namespace=Namespace.RSM)
rsm.addChild('max').setData(items_per_page)
if after is not None:
rsm.addChild('after').setData(after)
query = Iq(to=jid, typ='get')
query.addChild(node=search)
return query
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/nickname.py 0000664 0000000 0000000 00000010324 14130341156 0022753 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import PresenceType
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
from nbxmpp.task import iq_request_task
class Nickname(BaseModule):
_depends = {
'publish': 'PubSub'
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_nickname,
ns=Namespace.PUBSUB_EVENT,
priority=16),
StanzaHandler(name='message',
callback=self._process_nickname,
ns=Namespace.NICK,
priority=40),
StanzaHandler(name='presence',
callback=self._process_nickname,
ns=Namespace.NICK,
priority=40),
]
def _process_nickname(self, _client, stanza, properties):
if stanza.getName() == 'message':
properties.nickname = self._parse_nickname(stanza)
elif stanza.getName() == 'presence':
# the nickname MUST NOT be included in presence broadcasts
# (i.e., stanzas with no 'type' attribute or
# of type "unavailable").
if properties.type in (PresenceType.AVAILABLE,
PresenceType.UNAVAILABLE):
return
properties.nickname = self._parse_nickname(stanza)
def _process_pubsub_nickname(self, _client, _stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.NICK:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
nick = self._parse_nickname(item)
if nick is None:
self._log.info('Received nickname: %s - nickname removed',
properties.jid)
return
self._log.info('Received nickname: %s - %s', properties.jid, nick)
properties.pubsub_event = properties.pubsub_event._replace(data=nick)
@staticmethod
def _parse_nickname(stanza):
nickname = stanza.getTag('nick', namespace=Namespace.NICK)
if nickname is None:
return None
return nickname.getData() or None
@iq_request_task
def set_nickname(self, nickname, public=False):
task = yield
access_model = 'open' if public else 'presence'
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
item = Node('nick', {'xmlns': Namespace.NICK})
if nickname is not None:
item.addData(nickname)
result = yield self.publish(Namespace.NICK,
item,
id_='current',
options=options,
force_node_options=True)
yield finalize(task, result)
@iq_request_task
def set_access_model(self, public):
task = yield
access_model = 'open' if public else 'presence'
result = yield self._client.get_module('PubSub').set_access_model(
Namespace.NICK, access_model)
yield finalize(task, result)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/omemo.py 0000664 0000000 0000000 00000037046 14130341156 0022314 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import Node
from nbxmpp.protocol import Message
from nbxmpp.util import b64decode
from nbxmpp.util import b64encode
from nbxmpp.util import from_xs_boolean
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import OMEMOMessage
from nbxmpp.structs import OMEMOBundle
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
from nbxmpp.modules.util import raise_if_error
from nbxmpp.task import iq_request_task
from nbxmpp.errors import MalformedStanzaError
class OMEMO(BaseModule):
_depends = {
'publish': 'PubSub',
'request_items': 'PubSub',
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_omemo_devicelist,
ns=Namespace.PUBSUB_EVENT,
priority=16),
StanzaHandler(name='message',
callback=self._process_omemo_message,
ns=Namespace.OMEMO_TEMP,
priority=7),
]
def _process_omemo_message(self, _client, stanza, properties):
try:
properties.omemo = _parse_omemo_message(stanza)
self._log.info('Received message')
except MalformedStanzaError as error:
self._log.warning(error)
self._log.warning(stanza)
return
def _process_omemo_devicelist(self, _client, stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.OMEMO_TEMP_DL:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
try:
devices = _parse_devicelist(item)
except MalformedStanzaError as error:
self._log.warning(error)
self._log.warning(stanza)
raise NodeProcessed
if not devices:
self._log.info('Received OMEMO devicelist: %s - no devices set',
properties.jid)
return
pubsub_event = properties.pubsub_event._replace(data=devices)
self._log.info('Received OMEMO devicelist: %s - %s',
properties.jid, devices)
properties.pubsub_event = pubsub_event
@iq_request_task
def set_devicelist(self, devicelist=None, public=True):
task = yield
self._log.info('Set devicelist: %s', devicelist)
access_model = 'open' if public else 'presence'
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
result = yield self.publish(Namespace.OMEMO_TEMP_DL,
_make_devicelist(devicelist),
id_='current',
options=options,
force_node_options=True)
yield finalize(task, result)
@iq_request_task
def request_devicelist(self, jid=None):
task = yield
items = yield self.request_items(Namespace.OMEMO_TEMP_DL,
max_items=1,
jid=jid)
raise_if_error(items)
if not items:
yield task.set_result(None)
yield _parse_devicelist(items[0])
@iq_request_task
def set_bundle(self, bundle, device_id, public=True):
task = yield
self._log.info('Set bundle')
access_model = 'open' if public else 'presence'
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
result = yield self.publish(
f'{Namespace.OMEMO_TEMP_BUNDLE}:{device_id}',
_make_bundle(bundle),
id_='current',
options=options,
force_node_options=True)
yield finalize(task, result)
@iq_request_task
def request_bundle(self, jid, device_id):
task = yield
self._log.info('Request bundle from: %s %s', jid, device_id)
items = yield self.request_items(
f'{Namespace.OMEMO_TEMP_BUNDLE}:{device_id}',
max_items=1,
jid=jid)
raise_if_error(items)
if not items:
yield task.set_result(None)
yield _parse_bundle(items[0])
def _parse_omemo_message(stanza):
'''
BASE64ENCODED...BASE64ENCODED...BASE64ENCODED...BASE64ENCODED
'''
encrypted = stanza.getTag('encrypted', namespace=Namespace.OMEMO_TEMP)
if encrypted is None:
raise MalformedStanzaError('No encrypted node found', stanza)
header = encrypted.getTag('header')
if header is None:
raise MalformedStanzaError('header node not found', stanza)
try:
sid = int(header.getAttr('sid'))
except Exception as error:
raise MalformedStanzaError('sid attr not found', stanza)
iv_node = header.getTag('iv')
try:
iv = b64decode(iv_node.getData(), bytes)
except Exception as error:
raise MalformedStanzaError('failed to decode iv: %s' % error, stanza)
payload = None
payload_node = encrypted.getTag('payload')
if payload_node is not None:
try:
payload = b64decode(payload_node.getData(), bytes)
except Exception as error:
raise MalformedStanzaError('failed to decode payload: %s' % error,
stanza)
key_nodes = header.getTags('key')
if not key_nodes:
raise MalformedStanzaError('no keys found', stanza)
keys = {}
for kn in key_nodes:
rid = kn.getAttr('rid')
if rid is None:
raise MalformedStanzaError('rid not found', stanza)
prekey = kn.getAttr('prekey')
if prekey is None:
prekey = False
else:
try:
prekey = from_xs_boolean(prekey)
except ValueError as error:
raise MalformedStanzaError(error, stanza)
try:
keys[int(rid)] = (b64decode(kn.getData(), bytes), prekey)
except Exception as error:
raise MalformedStanzaError('failed to decode key: %s' % error,
stanza)
return OMEMOMessage(sid=sid, iv=iv, keys=keys, payload=payload)
def _parse_bundle(item):
'''
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
'''
if item is None:
raise MalformedStanzaError('No item in node found', item)
bundle = item.getTag('bundle', namespace=Namespace.OMEMO_TEMP)
if bundle is None:
raise MalformedStanzaError('No bundle node found', item)
result = {}
signed_prekey_node = bundle.getTag('signedPreKeyPublic')
try:
result['spk'] = {'key': b64decode(signed_prekey_node.getData(),
bytes)}
except Exception as error:
error = 'Failed to decode signedPreKeyPublic: %s' % error
raise MalformedStanzaError(error, item)
signed_prekey_id = signed_prekey_node.getAttr('signedPreKeyId')
try:
result['spk']['id'] = int(signed_prekey_id)
except Exception as error:
raise MalformedStanzaError('Invalid signedPreKeyId: %s' % error, item)
signed_signature_node = bundle.getTag('signedPreKeySignature')
try:
result['spk_signature'] = b64decode(signed_signature_node.getData(),
bytes)
except Exception as error:
error = 'Failed to decode signedPreKeySignature: %s' % error
raise MalformedStanzaError(error, item)
identity_key_node = bundle.getTag('identityKey')
try:
result['ik'] = b64decode(identity_key_node.getData(), bytes)
except Exception as error:
error = 'Failed to decode IdentityKey: %s' % error
raise MalformedStanzaError(error, item)
prekeys = bundle.getTag('prekeys')
if prekeys is None or not prekeys.getChildren():
raise MalformedStanzaError('No prekeys node found', item)
result['otpks'] = []
for prekey in prekeys.getChildren():
try:
id_ = int(prekey.getAttr('preKeyId'))
except Exception as error:
raise MalformedStanzaError('Invalid prekey: %s' % error, item)
try:
key = b64decode(prekey.getData(), bytes)
except Exception as error:
raise MalformedStanzaError(
'Failed to decode preKeyPublic: %s' % error, item)
result['otpks'].append({'key': key, 'id': id_})
return OMEMOBundle(**result)
def _make_bundle(bundle):
'''
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
BASE64ENCODED...
'''
bundle_node = Node('bundle', attrs={'xmlns': Namespace.OMEMO_TEMP})
prekey_pub_node = bundle_node.addChild(
'signedPreKeyPublic',
attrs={'signedPreKeyId': bundle.spk['id']})
prekey_pub_node.addData(b64encode(bundle.spk['key']))
prekey_sig_node = bundle_node.addChild('signedPreKeySignature')
prekey_sig_node.addData(b64encode(bundle.spk_signature))
identity_key_node = bundle_node.addChild('identityKey')
identity_key_node.addData(b64encode(bundle.ik))
prekeys = bundle_node.addChild('prekeys')
for key in bundle.otpks:
pre_key_public = prekeys.addChild('preKeyPublic',
attrs={'preKeyId': key['id']})
pre_key_public.addData(b64encode(key['key']))
return bundle_node
def _make_devicelist(devicelist):
if devicelist is None:
devicelist = []
devicelist_node = Node('list', attrs={'xmlns': Namespace.OMEMO_TEMP})
for device in devicelist:
devicelist_node.addChild('device').setAttr('id', device)
return devicelist_node
def _parse_devicelist(item):
'''
'''
list_node = item.getTag('list', namespace=Namespace.OMEMO_TEMP)
if list_node is None:
raise MalformedStanzaError('No list node found', item)
if not list_node.getChildren():
return []
result = []
devices_nodes = list_node.getChildren()
for dn in devices_nodes:
_id = dn.getAttr('id')
if _id:
result.append(int(_id))
return result
def create_omemo_message(stanza, omemo_message, store_hint=True,
node_whitelist=None):
'''
BASE64ENCODED...BASE64ENCODED...BASE64ENCODED...BASE64ENCODED
'''
if node_whitelist is not None:
cleanup_stanza(stanza, node_whitelist)
encrypted = Node('encrypted', attrs={'xmlns': Namespace.OMEMO_TEMP})
header = Node('header', attrs={'sid': omemo_message.sid})
for rid, (key, prekey) in omemo_message.keys.items():
attrs = {'rid': rid}
if prekey:
attrs['prekey'] = 'true'
child = header.addChild('key', attrs=attrs)
child.addData(b64encode(key))
header.addChild('iv').addData(b64encode(omemo_message.iv))
encrypted.addChild(node=header)
payload = encrypted.addChild('payload')
payload.addData(b64encode(omemo_message.payload))
stanza.addChild(node=encrypted)
stanza.addChild(node=Node('encryption',
attrs={'xmlns': Namespace.EME,
'name': 'OMEMO',
'namespace': Namespace.OMEMO_TEMP}))
stanza.setBody("You received a message encrypted with "
"OMEMO but your client doesn't support OMEMO.")
if store_hint:
stanza.addChild(node=Node('store', attrs={'xmlns': Namespace.HINTS}))
def get_key_transport_message(typ, jid, omemo_message):
message = Message(typ=typ, to=jid)
encrypted = Node('encrypted', attrs={'xmlns': Namespace.OMEMO_TEMP})
header = Node('header', attrs={'sid': omemo_message.sid})
for rid, (key, prekey) in omemo_message.keys.items():
attrs = {'rid': rid}
if prekey:
attrs['prekey'] = 'true'
child = header.addChild('key', attrs=attrs)
child.addData(b64encode(key))
header.addChild('iv').addData(b64encode(omemo_message.iv))
encrypted.addChild(node=header)
message.addChild(node=encrypted)
return message
def cleanup_stanza(stanza, node_whitelist):
whitelisted_nodes = []
for tag, ns in node_whitelist:
node = stanza.getTag(tag, namespace=ns)
if node is not None:
whitelisted_nodes.append(node)
for node in list(stanza.getChildren()):
stanza.delChild(node)
for node in whitelisted_nodes:
stanza.addChild(node=node)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/oob.py 0000664 0000000 0000000 00000003205 14130341156 0021745 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import OOBData
from nbxmpp.modules.base import BaseModule
class OOB(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_oob,
ns=Namespace.X_OOB,
priority=15),
]
def _process_message_oob(self, _client, stanza, properties):
oob = stanza.getTag('x', namespace=Namespace.X_OOB)
if oob is None:
return
url = oob.getTagData('url')
if url is None:
self._log.warning('OOB data without url')
self._log.warning(stanza)
return
desc = oob.getTagData('desc')
properties.oob = OOBData(url, desc)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/openpgp.py 0000664 0000000 0000000 00000032670 14130341156 0022646 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import time
import random
import string
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import Node
from nbxmpp.protocol import StanzaMalformed
from nbxmpp.protocol import JID
from nbxmpp.util import b64decode
from nbxmpp.util import b64encode
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import PGPKeyMetadata
from nbxmpp.structs import PGPPublicKey
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.date_and_time import parse_datetime
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
from nbxmpp.modules.util import raise_if_error
class OpenPGP(BaseModule):
_depends = {
'publish': 'PubSub',
'request_items': 'PubSub',
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_openpgp,
ns=Namespace.PUBSUB_EVENT,
priority=16),
StanzaHandler(name='message',
callback=self._process_openpgp_message,
ns=Namespace.OPENPGP,
priority=7),
]
def _process_openpgp_message(self, _client, stanza, properties):
openpgp = stanza.getTag('openpgp', namespace=Namespace.OPENPGP)
if openpgp is None:
self._log.warning('No openpgp node found')
self._log.warning(stanza)
return
data = openpgp.getData()
if not data:
self._log.warning('No data in openpgp node found')
self._log.warning(stanza)
return
self._log.info('Encrypted message received')
try:
properties.openpgp = b64decode(data, return_type=bytes)
except Exception:
self._log.warning('b64decode failed')
self._log.warning(stanza)
return
def _process_pubsub_openpgp(self, _client, stanza, properties):
"""
"""
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.OPENPGP_PK:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
try:
data = _parse_keylist(properties.jid, item)
except ValueError as error:
self._log.warning(error)
self._log.warning(stanza)
raise NodeProcessed
if data is None:
self._log.info('Received PGP keylist: %s - no keys set',
properties.jid)
return
pubsub_event = properties.pubsub_event._replace(data=data)
self._log.info('Received PGP keylist: %s - %s', properties.jid, data)
properties.pubsub_event = pubsub_event
@iq_request_task
def set_keylist(self, keylist, public=True):
task = yield
access_model = 'open' if public else 'presence'
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
self._log.info('Set keylist: %s', keylist)
result = yield self.publish(Namespace.OPENPGP_PK,
_make_keylist(keylist),
id_='current',
options=options,
force_node_options=True)
yield finalize(task, result)
@iq_request_task
def set_public_key(self, key, fingerprint, date, public=True):
task = yield
self._log.info('Set public key')
access_model = 'open' if public else 'presence'
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
result = yield self.publish(f'{Namespace.OPENPGP_PK}:{fingerprint}',
_make_public_key(key, date),
id_='current',
options=options,
force_node_options=True)
yield finalize(task, result)
@iq_request_task
def request_public_key(self, jid, fingerprint):
task = yield
self._log.info('Request public key from: %s %s', jid, fingerprint)
items = yield self.request_items(
f'{Namespace.OPENPGP_PK}:{fingerprint}',
max_items=1,
jid=jid)
raise_if_error(items)
if not items:
yield task.set_result(None)
try:
key = _parse_public_key(jid, items[0])
except ValueError as error:
raise MalformedStanzaError(str(error), items)
yield key
@iq_request_task
def request_keylist(self, jid=None):
task = yield
self._log.info('Request keylist from: %s', jid)
items = yield self.request_items(
Namespace.OPENPGP_PK,
max_items=1,
jid=jid)
raise_if_error(items)
if not items:
yield task.set_result(None)
try:
keylist = _parse_keylist(jid, items[0])
except ValueError as error:
raise MalformedStanzaError(str(error), items)
self._log.info('Received keylist: %s', keylist)
yield keylist
@iq_request_task
def request_secret_key(self):
task = yield
self._log.info('Request secret key')
items = yield self.request_items(
Namespace.OPENPGP_SK,
max_items=1)
raise_if_error(items)
if not items:
yield task.set_result(None)
try:
secret_key = _parse_secret_key(items[0])
except ValueError as error:
raise MalformedStanzaError(str(error), items)
yield secret_key
@iq_request_task
def set_secret_key(self, secret_key):
task = yield
self._log.info('Set public key')
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': 'whitelist',
}
self._log.info('Set secret key')
result = yield self.publish(Namespace.OPENPGP_SK,
_make_secret_key(secret_key),
id_='current',
options=options,
force_node_options=True)
yield finalize(task, result)
def parse_signcrypt(stanza):
'''
f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv
This is a secret message.
'''
if (stanza.getName() != 'signcrypt' or
stanza.getNamespace() != Namespace.OPENPGP):
raise StanzaMalformed('Invalid signcrypt node')
to_nodes = stanza.getTags('to')
if not to_nodes:
raise StanzaMalformed('missing to nodes')
recipients = []
for to_node in to_nodes:
jid = to_node.getAttr('jid')
try:
recipients.append(JID.from_string(jid))
except Exception as error:
raise StanzaMalformed('Invalid jid: %s %s' % (jid, error))
timestamp = stanza.getTagAttr('time', 'stamp')
if timestamp is None:
raise StanzaMalformed('Invalid timestamp')
payload = stanza.getTag('payload')
if payload is None or payload.getChildren() is None:
raise StanzaMalformed('Invalid payload node')
return payload.getChildren(), recipients, timestamp
def create_signcrypt_node(stanza, recipients, not_encrypted_nodes):
'''
f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv
This is a secret message.
'''
encrypted_nodes = []
child_nodes = list(stanza.getChildren())
for node in child_nodes:
if (node.getName(), node.getNamespace()) not in not_encrypted_nodes:
if not node.getNamespace():
node.setNamespace(Namespace.CLIENT)
encrypted_nodes.append(node)
stanza.delChild(node)
signcrypt = Node('signcrypt', attrs={'xmlns': Namespace.OPENPGP})
for recipient in recipients:
signcrypt.addChild('to', attrs={'jid': str(recipient)})
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
signcrypt.addChild('time', attrs={'stamp': timestamp})
signcrypt.addChild('rpad').addData(get_rpad())
payload = signcrypt.addChild('payload')
for node in encrypted_nodes:
payload.addChild(node=node)
return signcrypt
def get_rpad():
rpad_range = random.randint(30, 50)
return ''.join(
random.choice(string.ascii_letters) for _ in range(rpad_range))
def create_message_stanza(stanza, encrypted_payload, with_fallback_text):
b64encoded_payload = b64encode(encrypted_payload)
openpgp_node = Node('openpgp', attrs={'xmlns': Namespace.OPENPGP})
openpgp_node.addData(b64encoded_payload)
stanza.addChild(node=openpgp_node)
eme_node = Node('encryption', attrs={'xmlns': Namespace.EME,
'namespace': Namespace.OPENPGP})
stanza.addChild(node=eme_node)
if with_fallback_text:
stanza.setBody(
'[This message is *encrypted* with OpenPGP (See :XEP:`0373`]')
def _make_keylist(keylist):
item = Node('public-keys-list', {'xmlns': Namespace.OPENPGP})
if keylist is not None:
for key in keylist:
date = time.strftime('%Y-%m-%dT%H:%M:%SZ',
time.gmtime(key.date))
attrs = {'v4-fingerprint': key.fingerprint,
'date': date}
item.addChild('pubkey-metadata', attrs=attrs)
return item
def _make_public_key(key, date):
date = time.strftime(
'%Y-%m-%dT%H:%M:%SZ', time.gmtime(date))
item = Node('pubkey', attrs={'xmlns': Namespace.OPENPGP,
'date': date})
data = item.addChild('data')
data.addData(b64encode(key))
return item
def _make_secret_key(secret_key):
item = Node('secretkey', {'xmlns': Namespace.OPENPGP})
if secret_key is not None:
item.setData(b64encode(secret_key))
return item
def _parse_public_key(jid, item):
pub_key = item.getTag('pubkey', namespace=Namespace.OPENPGP)
if pub_key is None:
raise ValueError('pubkey node missing')
date = parse_datetime(pub_key.getAttr('date'), epoch=True)
data = pub_key.getTag('data')
if data is None:
raise ValueError('data node missing')
try:
key = b64decode(data.getData(), return_type=bytes)
except Exception as error:
raise ValueError(f'decoding error: {error}')
return PGPPublicKey(jid, key, date)
def _parse_keylist(jid, item):
keylist_node = item.getTag('public-keys-list',
namespace=Namespace.OPENPGP)
if keylist_node is None:
raise ValueError('public-keys-list node missing')
metadata = keylist_node.getTags('pubkey-metadata')
if not metadata:
return None
data = []
for key in metadata:
fingerprint = key.getAttr('v4-fingerprint')
date = key.getAttr('date')
if fingerprint is None or date is None:
raise ValueError('Invalid metadata node')
timestamp = parse_datetime(date, epoch=True)
if timestamp is None:
raise ValueError('Invalid date timestamp: %s' % date)
data.append(PGPKeyMetadata(jid, fingerprint, timestamp))
return data
def _parse_secret_key(item):
sec_key = item.getTag('secretkey', namespace=Namespace.OPENPGP)
if sec_key is None:
raise ValueError('secretkey node missing')
data = sec_key.getData()
if not data:
raise ValueError('secretkey data missing')
try:
key = b64decode(data, return_type=bytes)
except Exception as error:
raise ValueError(f'decoding error: {error}')
return key
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/pgplegacy.py 0000664 0000000 0000000 00000004204 14130341156 0023141 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.base import BaseModule
class PGPLegacy(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pgplegacy_message,
ns=Namespace.ENCRYPTED,
priority=7),
StanzaHandler(name='presence',
callback=self._process_signed,
ns=Namespace.SIGNED,
priority=15)
]
@staticmethod
def _process_signed(_client, stanza, properties):
signed = stanza.getTag('x', namespace=Namespace.SIGNED)
if signed is None:
return
properties.signed = signed.getData()
def _process_pgplegacy_message(self, _client, stanza, properties):
pgplegacy = stanza.getTag('x', namespace=Namespace.ENCRYPTED)
if pgplegacy is None:
self._log.warning('No x node found')
self._log.warning(stanza)
return
data = pgplegacy.getData()
if not data:
self._log.warning('No data in x node found')
self._log.warning(stanza)
return
self._log.info('Encrypted message received')
properties.pgp_legacy = data
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/ping.py 0000664 0000000 0000000 00000003632 14130341156 0022127 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.protocol import Iq
from nbxmpp.protocol import NodeProcessed
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.task import iq_request_task
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import process_response
class Ping(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='iq',
callback=self._process_ping,
typ='get',
ns=Namespace.PING,
priority=15),
]
def _process_ping(self, _client, stanza, properties):
self._log.info('Send pong to %s', stanza.getFrom())
iq = stanza.buildSimpleReply('result')
self._client.send_stanza(iq)
raise NodeProcessed
@iq_request_task
def ping(self, jid):
_task = yield
self._log.info('Ping')
response = yield _make_ping_request(jid)
yield process_response(response)
def _make_ping_request(jid):
iq = Iq('get', to=jid)
iq.addChild(name='ping', namespace=Namespace.PING)
return iq
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/presence.py 0000664 0000000 0000000 00000006301 14130341156 0022772 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.protocol import Error as ErrorStanza
from nbxmpp.protocol import ERR_BAD_REQUEST
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.util import error_factory
from nbxmpp.const import PresenceType
from nbxmpp.const import PresenceShow
from nbxmpp.modules.base import BaseModule
class BasePresence(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='presence',
callback=self._process_presence_base,
priority=10),
]
def _process_presence_base(self, _client, stanza, properties):
properties.type = self._parse_type(stanza)
properties.priority = self._parse_priority(stanza)
properties.show = self._parse_show(stanza)
properties.jid = stanza.getFrom()
properties.id = stanza.getID()
properties.status = stanza.getStatus()
if properties.type.is_error:
properties.error = error_factory(stanza)
own_jid = self._client.get_bound_jid()
properties.self_presence = own_jid == properties.jid
properties.self_bare = properties.jid.bare_match(own_jid)
def _parse_priority(self, stanza):
priority = stanza.getPriority()
if priority is None:
return 0
try:
priority = int(priority)
except Exception:
self._log.warning('Invalid priority value: %s', priority)
self._log.warning(stanza)
return 0
if priority not in range(-129, 128):
self._log.warning('Invalid priority value: %s', priority)
self._log.warning(stanza)
return 0
return priority
def _parse_type(self, stanza):
type_ = stanza.getType()
try:
return PresenceType(type_)
except ValueError:
self._log.warning('Presence with invalid type received')
self._log.warning(stanza)
self._client.send_stanza(ErrorStanza(stanza, ERR_BAD_REQUEST))
raise NodeProcessed
def _parse_show(self, stanza):
show = stanza.getShow()
if show is None:
return PresenceShow.ONLINE
try:
return PresenceShow(stanza.getShow())
except ValueError:
self._log.warning('Presence with invalid show')
self._log.warning(stanza)
return PresenceShow.ONLINE
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/pubsub.py 0000664 0000000 0000000 00000033742 14130341156 0022477 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from collections import namedtuple
from nbxmpp.task import iq_request_task
from nbxmpp.errors import is_error
from nbxmpp.errors import PubSubStanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import PubSubEventData
from nbxmpp.structs import CommonResult
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import process_response
from nbxmpp.modules.util import raise_if_error
from nbxmpp.modules.util import finalize
from nbxmpp.modules.dataforms import extend_form
class PubSub(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_base,
ns=Namespace.PUBSUB_EVENT,
priority=15),
]
def _process_pubsub_base(self, _client, stanza, properties):
properties.pubsub = True
event = stanza.getTag('event', namespace=Namespace.PUBSUB_EVENT)
delete = event.getTag('delete')
if delete is not None:
node = delete.getAttr('node')
properties.pubsub_event = PubSubEventData(
node, deleted=True)
return
purge = event.getTag('purge')
if purge is not None:
node = purge.getAttr('node')
properties.pubsub_event = PubSubEventData(node, purged=True)
return
items = event.getTag('items')
if items is not None:
node = items.getAttr('node')
retract = items.getTag('retract')
if retract is not None:
id_ = retract.getAttr('id')
properties.pubsub_event = PubSubEventData(
node, id_, retracted=True)
return
if len(items.getChildren()) != 1:
self._log.warning('PubSub event with != 1 item')
self._log.warning(stanza)
return
item = items.getTag('item')
if item is None:
self._log.warning('No item node found')
self._log.warning(stanza)
return
id_ = item.getAttr('id')
properties.pubsub_event = PubSubEventData(node, id_, item)
@iq_request_task
def request_item(self, node, id_, jid=None):
task = yield
response = yield _make_pubsub_request(node, id_=id_, jid=jid)
if response.isError():
raise PubSubStanzaError(response)
item = _get_pubsub_item(response, node, id_)
yield task.set_result(item)
@iq_request_task
def request_items(self, node, max_items=None, jid=None):
_task = yield
response = yield _make_pubsub_request(node,
max_items=max_items,
jid=jid)
if response.isError():
raise PubSubStanzaError(response)
yield _get_pubsub_items(response, node)
@iq_request_task
def publish(self,
node,
item,
id_=None,
options=None,
jid=None,
force_node_options=False):
_task = yield
request = _make_publish_request(node, item, id_, options, jid)
response = yield request
if response.isError():
error = PubSubStanzaError(response)
if (not force_node_options or
error.app_condition != 'precondition-not-met'):
raise error
result = yield self.reconfigure_node(node, options, jid)
if is_error(result):
raise result
response = yield request
if response.isError():
raise PubSubStanzaError(response)
jid = response.getFrom()
item_id = _get_published_item_id(response, node, id_)
yield PubSubPublishResult(jid, node, item_id)
@iq_request_task
def get_access_model(self, node):
_task = yield
self._log.info('Request access model')
result = yield self.get_node_configuration(node)
raise_if_error(result)
yield result.form['pubsub#access_model'].value
@iq_request_task
def set_access_model(self, node, model):
task = yield
if model not in ('open', 'presence'):
raise ValueError('Invalid access model')
result = yield self.get_node_configuration(node)
raise_if_error(result)
try:
access_model = result.form['pubsub#access_model'].value
except Exception:
yield task.set_error('warning',
condition='access-model-not-supported')
if access_model == model:
jid = self._client.get_bound_jid().new_as_bare()
yield CommonResult(jid=jid)
result.form['pubsub#access_model'].value = model
self._log.info('Set access model %s', model)
result = yield self.set_node_configuration(node, result.form)
yield finalize(task, result)
@iq_request_task
def retract(self, node, id_, jid=None, notify=True):
_task = yield
response = yield _make_retract_request(node, id_, jid, notify)
yield process_response(response)
@iq_request_task
def delete(self, node, jid=None):
_task = yield
response = yield _make_delete_request(node, jid)
yield process_response(response)
@iq_request_task
def reconfigure_node(self, node, options, jid=None):
_task = yield
result = yield self.get_node_configuration(node, jid)
if is_error(result):
raise result
_apply_options(result.form, options)
result = yield self.set_node_configuration(node, result.form, jid)
yield result
@iq_request_task
def set_node_configuration(self, node, form, jid=None):
_task = yield
response = yield _make_node_configuration(node, form, jid)
yield process_response(response)
@iq_request_task
def get_node_configuration(self, node, jid=None):
_task = yield
response = yield _make_node_configuration_request(node, jid)
if response.isError():
raise PubSubStanzaError(response)
jid = response.getFrom()
form = _get_configure_form(response, node)
yield PubSubNodeConfigurationResult(jid=jid, node=node, form=form)
def get_pubsub_request(jid, node, id_=None, max_items=None):
query = Iq('get', to=jid)
pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
items = pubsub.addChild('items', {'node': node})
if max_items is not None:
items.setAttr('max_items', max_items)
if id_ is not None:
items.addChild('item', {'id': id_})
return query
def get_pubsub_item(stanza):
pubsub_node = stanza.getTag('pubsub')
items_node = pubsub_node.getTag('items')
return items_node.getTag('item')
def get_pubsub_items(stanza, node=None):
pubsub_node = stanza.getTag('pubsub')
items_node = pubsub_node.getTag('items')
if node is not None and items_node.getAttr('node') != node:
return None
if items_node is not None:
return items_node.getTags('item')
return None
def get_publish_options(config):
options = Node(Namespace.DATA + ' x', attrs={'type': 'submit'})
field = options.addChild('field',
attrs={'var': 'FORM_TYPE', 'type': 'hidden'})
field.setTagData('value', Namespace.PUBSUB_PUBLISH_OPTIONS)
for var, value in config.items():
field = options.addChild('field', attrs={'var': var})
field.setTagData('value', value)
return options
def _get_pubsub_items(response, node):
pubsub_node = response.getTag('pubsub', namespace=Namespace.PUBSUB)
if pubsub_node is None:
raise MalformedStanzaError('pubsub node missing', response)
items_node = pubsub_node.getTag('items')
if items_node is None:
raise MalformedStanzaError('items node missing', response)
if items_node.getAttr('node') != node:
raise MalformedStanzaError('invalid node attr', response)
return items_node.getTags('item')
def _get_pubsub_item(response, node, id_):
items = _get_pubsub_items(response, node)
if len(items) > 1:
raise MalformedStanzaError('multiple items found', response)
if not items:
return None
item = items[0]
if item.getAttr('id') != id_:
raise MalformedStanzaError('invalid item id', response)
return item
def _make_pubsub_request(node, id_=None, max_items=None, jid=None):
query = Iq('get', to=jid)
pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
items = pubsub.addChild('items', {'node': node})
if max_items is not None:
items.setAttr('max_items', max_items)
if id_ is not None:
items.addChild('item', {'id': id_})
return query
def _get_configure_form(response, node):
pubsub = response.getTag('pubsub', namespace=Namespace.PUBSUB_OWNER)
if pubsub is None:
raise MalformedStanzaError('pubsub node missing', response)
configure = pubsub.getTag('configure')
if configure is None:
raise MalformedStanzaError('configure node missing', response)
if node != configure.getAttr('node'):
raise MalformedStanzaError('invalid node attribute', response)
forms = configure.getTags('x', namespace=Namespace.DATA)
for form in forms:
dataform = extend_form(node=form)
form_type = dataform.vars.get('FORM_TYPE')
if form_type is None or form_type.value != Namespace.PUBSUB_CONFIG:
continue
return dataform
raise MalformedStanzaError('no valid form type found', response)
def _get_published_item_id(response, node, id_):
pubsub = response.getTag('pubsub', namespace=Namespace.PUBSUB)
if pubsub is None:
# https://xmpp.org/extensions/xep-0060.html#publisher-publish-success
# If the publish request did not include an ItemID,
# the IQ-result SHOULD include an empty element
# that specifies the ItemID of the published item.
#
# If the server did not add a payload we assume the item was
# published with the id we requested
return id_
publish = pubsub.getTag('publish')
if publish is None:
raise MalformedStanzaError('publish node missing', response)
if node != publish.getAttr('node'):
raise MalformedStanzaError('invalid node attribute', response)
item = publish.getTag('item')
if item is None:
raise MalformedStanzaError('item node missing', response)
item_id = item.getAttr('id')
if id_ is not None and item_id != id_:
raise MalformedStanzaError('invalid item id', response)
return item_id
def _make_publish_request(node, item, id_, options, jid):
query = Iq('set', to=jid)
pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
publish = pubsub.addChild('publish', {'node': node})
attrs = {}
if id_ is not None:
attrs = {'id': id_}
publish.addChild('item', attrs, [item])
if options:
publish = pubsub.addChild('publish-options')
publish.addChild(node=_make_publish_options(options))
return query
def _make_publish_options(options):
data = Node(Namespace.DATA + ' x', attrs={'type': 'submit'})
field = data.addChild('field', attrs={'var': 'FORM_TYPE', 'type': 'hidden'})
field.setTagData('value', Namespace.PUBSUB_PUBLISH_OPTIONS)
for var, value in options.items():
field = data.addChild('field', attrs={'var': var})
field.setTagData('value', value)
return data
def _make_retract_request(node, id_, jid, notify):
query = Iq('set', to=jid)
pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB)
attrs = {'node': node}
if notify:
attrs['notify'] = 'true'
retract = pubsub.addChild('retract', attrs=attrs)
retract.addChild('item', {'id': id_})
return query
def _make_delete_request(node, jid):
query = Iq('set', to=jid)
pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER)
pubsub.addChild('delete', attrs={'node': node})
return query
def _make_node_configuration(node, form, jid):
query = Iq('set', to=jid)
pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER)
configure = pubsub.addChild('configure', {'node': node})
form.setAttr('type', 'submit')
configure.addChild(node=form)
return query
def _make_node_configuration_request(node, jid):
query = Iq('get', to=jid)
pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER)
pubsub.addChild('configure', {'node': node})
return query
def _apply_options(form, options):
for var, value in options.items():
try:
field = form[var]
except KeyError:
pass
else:
field.value = value
PubSubNodeConfigurationResult = namedtuple('PubSubConfigResult',
'jid node form')
PubSubConfigResult = namedtuple('PubSubConfigResult',
'jid node form')
PubSubPublishResult = namedtuple('PubSubPublishResult',
'jid node id')
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/receipts.py 0000664 0000000 0000000 00000005652 14130341156 0023014 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import isMucPM
from nbxmpp.protocol import Message
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import ReceiptData
from nbxmpp.util import generate_id
from nbxmpp.modules.base import BaseModule
class Receipts(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_receipt,
ns=Namespace.RECEIPTS,
priority=15),
]
def _process_message_receipt(self, _client, stanza, properties):
request = stanza.getTag('request', namespace=Namespace.RECEIPTS)
if request is not None:
properties.receipt = ReceiptData(request.getName())
return
received = stanza.getTag('received', namespace=Namespace.RECEIPTS)
if received is not None:
id_ = received.getAttr('id')
if id_ is None:
self._log.warning('Receipt without id attr')
self._log.warning(stanza)
return
properties.receipt = ReceiptData(received.getName(), id_)
def build_receipt(stanza):
if not isinstance(stanza, Message):
raise ValueError('Stanza type must be protocol.Message')
if stanza.getType() == 'error':
raise ValueError('Receipt can not be generated for type error messages')
if stanza.getID() is None:
raise ValueError('Receipt can not be generated for messages without id')
if stanza.getTag('received', namespace=Namespace.RECEIPTS) is not None:
raise ValueError('Receipt can not be generated for receipts')
is_muc_pm = isMucPM(stanza)
jid = stanza.getFrom()
typ = stanza.getType()
if typ == 'groupchat' or not is_muc_pm:
jid = jid.new_as_bare()
message = Message(to=jid, typ=typ)
if is_muc_pm:
message.setTag('x', namespace=Namespace.MUC_USER)
message_id = generate_id()
message.setID(message_id)
message.setReceiptReceived(stanza.getID())
message.setHint('store')
message.setOriginID(message_id)
return message
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/register/ 0000775 0000000 0000000 00000000000 14130341156 0022440 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/register/__init__.py 0000664 0000000 0000000 00000000037 14130341156 0024551 0 ustar 00root root 0000000 0000000 from .register import Register
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/register/register.py 0000664 0000000 0000000 00000006600 14130341156 0024640 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.util import get_form
from nbxmpp.task import iq_request_task
from nbxmpp.errors import StanzaError
from nbxmpp.errors import RegisterStanzaError
from nbxmpp.errors import ChangePasswordStanzaError
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import process_response
from nbxmpp.modules.register.util import _make_unregister_request
from nbxmpp.modules.register.util import _make_register_form
from nbxmpp.modules.register.util import _make_password_change_request
from nbxmpp.modules.register.util import _make_password_change_with_form
from nbxmpp.modules.register.util import _parse_register_data
class Register(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def unregister(self, jid=None):
_task = yield
response = yield _make_unregister_request(jid)
yield process_response(response)
@iq_request_task
def request_register_form(self, jid=None):
_task = yield
if jid is None:
jid = self._client.domain
response = yield Iq('get', Namespace.REGISTER, to=jid)
if response.isError():
raise StanzaError(response)
yield _parse_register_data(response)
@iq_request_task
def submit_register_form(self, form, jid=None):
_task = yield
if jid is None:
jid = self._client.domain
response = yield _make_register_form(jid, form)
if not response.isError():
yield process_response(response)
else:
data = _parse_register_data(response)
raise RegisterStanzaError(response, data)
@iq_request_task
def change_password(self, password):
_task = yield
response = yield _make_password_change_request(
self._client.domain, self._client.username, password)
if not response.isError():
yield process_response(response)
else:
query = response.getQuery()
if query is None:
raise StanzaError(response)
form = get_form(query, 'jabber:iq:register:changepassword')
if form is None or response.getType() != 'modify':
raise StanzaError(response)
raise ChangePasswordStanzaError(response, form)
@iq_request_task
def change_password_with_form(self, form):
_task = yield
response = yield _make_password_change_with_form(self._client.domain,
form)
yield process_response(response)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/register/util.py 0000664 0000000 0000000 00000010211 14130341156 0023762 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.const import REGISTER_FIELDS
from nbxmpp.structs import RegisterData
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.modules.dataforms import create_field
from nbxmpp.modules.dataforms import extend_form
from nbxmpp.modules.dataforms import SimpleDataForm
from nbxmpp.modules.bits_of_binary import parse_bob_data
def _make_password_change_request(domain, username, password):
iq = Iq('set', Namespace.REGISTER, to=domain)
query = iq.getQuery()
query.setTagData('username', username)
query.setTagData('password', password)
return iq
def _make_password_change_with_form(domain, form):
iq = Iq('set', Namespace.REGISTER, to=domain)
iq.setQueryPayload(form)
return iq
def _make_register_form(jid, form):
iq = Iq('set', Namespace.REGISTER, to=jid)
if form.is_fake_form():
query = iq.getTag('query')
for field in form.iter_fields():
if field.var == 'fakeform':
continue
query.addChild(field.var, payload=[field.value])
return iq
iq.setQueryPayload(form)
return iq
def _make_unregister_request(jid):
iq = Iq('set', to=jid)
query = iq.setQuery()
query.setNamespace(Namespace.REGISTER)
query.addChild('remove')
return iq
def _parse_oob_url(query):
oob = query.getTag('x', namespace=Namespace.X_OOB)
if oob is not None:
return oob.getTagData('url') or None
return None
def _parse_form(stanza):
query = stanza.getTag('query', namespace=Namespace.REGISTER)
form = query.getTag('x', namespace=Namespace.DATA)
if form is None:
return None
form = extend_form(node=form)
field = form.vars.get('FORM_TYPE')
if field is None:
return None
# Invalid urn:xmpp:captcha used by ejabberd
# See https://github.com/processone/ejabberd/issues/3045
if field.value in ('jabber:iq:register', 'urn:xmpp:captcha'):
return form
return None
def _parse_fields_form(query):
fields = []
for field in query.getChildren():
field_name = field.getName()
if field_name not in REGISTER_FIELDS:
continue
required = field_name in ('username', 'password')
typ = 'text-single' if field_name != 'password' else 'text-private'
fields.append(create_field(typ=typ,
var=field_name,
required=required))
if not fields:
return None
fields.append(create_field(typ='hidden', var='fakeform'))
return SimpleDataForm(type_='form',
instructions=query.getTagData('instructions'),
fields=fields)
def _parse_register_data(response):
query = response.getTag('query', namespace=Namespace.REGISTER)
if query is None:
raise StanzaError(response)
instructions = query.getTagData('instructions') or None
data = RegisterData(instructions=instructions,
form=_parse_form(response),
fields_form=_parse_fields_form(query),
oob_url=_parse_oob_url(query),
bob_data=parse_bob_data(query))
if (data.form is None and
data.fields_form is None and
data.oob_url is None):
raise MalformedStanzaError('invalid register response', response)
return data
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/rsm.py 0000664 0000000 0000000 00000003466 14130341156 0022000 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import RSMData
def parse_rsm(stanza):
stanza = stanza.getTag('set', namespace=Namespace.RSM)
if stanza is None:
return None
after = stanza.getTagData('after') or None
before = stanza.getTagData('before') or None
last = stanza.getTagData('last') or None
first_index = None
first = stanza.getTagData('first') or None
if first is not None:
try:
first_index = int(first.getAttr('index'))
except Exception:
pass
try:
count = int(stanza.getTagData('count'))
except Exception:
count = None
try:
max_ = int(stanza.getTagData('max'))
except Exception:
max_ = None
try:
index = int(stanza.getTagData('index'))
except Exception:
index = None
return RSMData(after=after,
before=before,
last=last,
first=first,
first_index=first_index,
count=count,
max=max_,
index=index)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/security_labels.py 0000664 0000000 0000000 00000011334 14130341156 0024361 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from typing import Dict
from dataclasses import dataclass
from nbxmpp.protocol import Iq
from nbxmpp.simplexml import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.base import BaseModule
class SecurityLabels(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_security_label,
ns=Namespace.SECLABEL,
priority=15),
]
def _process_message_security_label(self, _client, stanza, properties):
security = stanza.getTag('securitylabel', namespace=Namespace.SECLABEL)
if security is None:
return
try:
security_label = SecurityLabel.from_node(security)
except ValueError as error:
self._log.warning(error)
return
properties.security_label = security_label
@iq_request_task
def request_catalog(self, jid):
_task = yield
response = yield _make_catalog_request(self._client.domain, jid)
if response.isError():
raise StanzaError(response)
catalog_node = response.getTag('catalog',
namespace=Namespace.SECLABEL_CATALOG)
to = catalog_node.getAttr('to')
items = catalog_node.getTags('item')
labels = {}
default = None
for item in items:
label = item.getAttr('selector')
if label is None:
continue
security = item.getTag('securitylabel',
namespace=Namespace.SECLABEL)
if security is None:
continue
try:
security_label = SecurityLabel.from_node(security)
except ValueError:
continue
labels[label] = security_label
if item.getAttr('default') == 'true':
default = label
yield Catalog(labels=labels, default=default)
def _make_catalog_request(domain, jid):
iq = Iq(typ='get', to=domain)
iq.addChild(name='catalog',
namespace=Namespace.SECLABEL_CATALOG,
attrs={'to': jid})
return iq
@dataclass
class Displaymarking:
name: str
fgcolor: str
bgcolor: str
def to_node(self):
displaymarking = Node(tag='displaymarking')
if self.fgcolor and self.fgcolor != '#000':
displaymarking.setAttr('fgcolor', self.fgcolor)
if self.bgcolor and self.bgcolor != '#FFF':
displaymarking.setAttr('bgcolor', self.bgcolor)
if self.name:
displaymarking.setData(self.name)
return displaymarking
@classmethod
def from_node(cls, node):
return cls(name=node.getData(),
fgcolor=node.getAttr('fgcolor') or '#000',
bgcolor=node.getAttr('bgcolor') or '#FFF')
@dataclass
class SecurityLabel:
displaymarking: Displaymarking
label: Node
def to_node(self):
security = Node(tag='securitylabel',
attrs={'xmlns': Namespace.SECLABEL})
if self.displaymarking is not None:
security.addChild(node=self.displaymarking.to_node())
security.addChild(node=self.label)
return security
@classmethod
def from_node(cls, security):
displaymarking = security.getTag('displaymarking')
if displaymarking is not None:
displaymarking = Displaymarking.from_node(displaymarking)
label = security.getTag('label')
if label is None:
raise ValueError('label node missing')
return cls(displaymarking=displaymarking, label=label)
@dataclass
class Catalog:
labels: Dict[str, SecurityLabel]
default: str
def get_label_names(self):
return list(self.labels.keys())
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/software_version.py 0000664 0000000 0000000 00000006776 14130341156 0024605 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Iq
from nbxmpp.protocol import ErrorNode
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import ERR_SERVICE_UNAVAILABLE
from nbxmpp.structs import SoftwareVersionResult
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.base import BaseModule
from nbxmpp.task import iq_request_task
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import StanzaError
class SoftwareVersion(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='iq',
callback=self._answer_request,
typ='get',
ns=Namespace.VERSION),
]
self._name = None
self._version = None
self._os = None
self._enabled = False
def disable(self):
self._enabled = False
@iq_request_task
def request_software_version(self, jid):
_task = yield
self._log.info('Request software version for %s', jid)
response = yield Iq(typ='get', to=jid, queryNS=Namespace.VERSION)
if response.isError():
raise StanzaError(response)
yield _parse_info(response)
def set_software_version(self, name, version, os=None):
self._name, self._version, self._os = name, version, os
self._enabled = True
def _answer_request(self, _con, stanza, _properties):
self._log.info('Request received from %s', stanza.getFrom())
if (not self._enabled or
self._name is None or
self._version is None):
iq = stanza.buildReply('error')
iq.addChild(node=ErrorNode(ERR_SERVICE_UNAVAILABLE))
self._log.info('Send service-unavailable')
else:
iq = stanza.buildReply('result')
query = iq.getQuery()
query.setTagData('name', self._name)
query.setTagData('version', self._version)
if self._os is not None:
query.setTagData('os', self._os)
self._log.info('Send software version: %s %s %s',
self._name, self._version, self._os)
self._client.send_stanza(iq)
raise NodeProcessed
def _parse_info(stanza):
try:
name = stanza.getQueryChild('name').getData()
except Exception:
raise MalformedStanzaError('name node missing', stanza)
try:
version = stanza.getQueryChild('version').getData()
except Exception:
raise MalformedStanzaError('version node missing', stanza)
os_info = stanza.getQueryChild('os')
if os_info is not None:
os_info = os_info.getData()
return SoftwareVersionResult(name, version, os_info)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/tune.py 0000664 0000000 0000000 00000005625 14130341156 0022151 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import TuneData
from nbxmpp.const import TUNE_DATA
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import finalize
from nbxmpp.task import iq_request_task
class Tune(BaseModule):
_depends = {
'publish': 'PubSub'
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_tune,
ns=Namespace.PUBSUB_EVENT,
priority=16),
]
def _process_pubsub_tune(self, _client, _stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.TUNE:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
tune_node = item.getTag('tune', namespace=Namespace.TUNE)
if not tune_node.getChildren():
self._log.info('Received tune: %s - no tune set', properties.jid)
return
tune_dict = {}
for attr in TUNE_DATA:
tune_dict[attr] = tune_node.getTagData(attr)
data = TuneData(**tune_dict)
if data.artist is None and data.title is None:
self._log.warning('Missing artist or title: %s %s',
data, properties.jid)
return
pubsub_event = properties.pubsub_event._replace(data=data)
self._log.info('Received tune: %s - %s', properties.jid, data)
properties.pubsub_event = pubsub_event
@iq_request_task
def set_tune(self, data):
task = yield
item = Node('tune', {'xmlns': Namespace.TUNE})
if data is not None:
data = data._asdict()
for tag, value in data.items():
if value is not None:
item.addChild(tag, payload=value)
result = yield self.publish(Namespace.TUNE, item, id_='current')
yield finalize(task, result)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/user_avatar.py 0000664 0000000 0000000 00000025357 14130341156 0023516 0 ustar 00root root 0000000 0000000 # Copyright (C) 2019 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from typing import List
from typing import Dict
import hashlib
from dataclasses import dataclass
from dataclasses import asdict
from dataclasses import field
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import Node
from nbxmpp.structs import StanzaHandler
from nbxmpp.util import b64encode
from nbxmpp.util import b64decode
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import raise_if_error
from nbxmpp.modules.util import finalize
class UserAvatar(BaseModule):
_depends = {
'publish': 'PubSub',
'request_item': 'PubSub',
'request_items': 'PubSub',
'delete': 'PubSub',
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='message',
callback=self._process_pubsub_avatar,
ns=Namespace.PUBSUB_EVENT,
priority=16),
]
def _process_pubsub_avatar(self, _client, stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != Namespace.AVATAR_METADATA:
return
item = properties.pubsub_event.item
if item is None:
# Retract, Deleted or Purged
return
metadata = item.getTag('metadata', namespace=Namespace.AVATAR_METADATA)
if metadata is None:
self._log.warning('No metadata node found')
self._log.warning(stanza)
raise NodeProcessed
if not metadata.getChildren():
self._log.info('Received avatar metadata: %s - no avatar set',
properties.jid)
return
try:
data = AvatarMetaData.from_node(metadata, item.getAttr('id'))
except Exception as error:
self._log.warning('Malformed user avatar data: %s', error)
self._log.warning(stanza)
raise NodeProcessed
pubsub_event = properties.pubsub_event._replace(data=data)
self._log.info('Received avatar metadata: %s - %s',
properties.jid, data)
properties.pubsub_event = pubsub_event
@iq_request_task
def request_avatar_data(self, id_, jid=None):
task = yield
item = yield self.request_item(Namespace.AVATAR_DATA,
id_=id_,
jid=jid)
raise_if_error(item)
if item is None:
yield task.set_result(None)
yield _get_avatar_data(item, id_)
@iq_request_task
def request_avatar_metadata(self, jid=None):
task = yield
items = yield self.request_items(Namespace.AVATAR_METADATA,
max_items=1,
jid=jid)
raise_if_error(items)
if not items:
yield task.set_result(None)
item = items[0]
metadata = item.getTag('metadata', namespace=Namespace.AVATAR_METADATA)
if metadata is None:
raise MalformedStanzaError('metadata node missing', item)
if not metadata.getChildren():
yield task.set_result(None)
yield AvatarMetaData.from_node(metadata, item.getAttr('id'))
@iq_request_task
def set_avatar(self, avatar, public=False):
task = yield
access_model = 'open' if public else 'presence'
if avatar is None:
result = yield self._publish_avatar_metadata(None, access_model)
raise_if_error(result)
result = yield self.delete(Namespace.AVATAR_DATA)
yield finalize(task, result)
result = yield self._publish_avatar(avatar, access_model)
yield finalize(task, result)
@iq_request_task
def _publish_avatar(self, avatar, access_model):
task = yield
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
for info, data in avatar.pubsub_avatar_info():
item = _make_avatar_data_node(data)
self._log.info('Publish avatar data: %s, %s', info, access_model)
result = yield self.publish(Namespace.AVATAR_DATA,
item,
id_=info.id,
options=options,
force_node_options=True)
raise_if_error(result)
result = yield self._publish_avatar_metadata(avatar.metadata,
access_model)
yield finalize(task, result)
@iq_request_task
def _publish_avatar_metadata(self, metadata, access_model):
task = yield
self._log.info('Publish avatar meta data: %s', metadata)
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
if metadata is None:
metadata = AvatarMetaData()
result = yield self.publish(Namespace.AVATAR_METADATA,
metadata.to_node(),
id_=metadata.default,
options=options,
force_node_options=True)
yield finalize(task, result)
@iq_request_task
def set_access_model(self, public):
task = yield
access_model = 'open' if public else 'presence'
result = yield self._client.get_module('PubSub').set_access_model(
Namespace.AVATAR_DATA, access_model)
raise_if_error(result)
result = yield self._client.get_module('PubSub').set_access_model(
Namespace.AVATAR_METADATA, access_model)
yield finalize(task, result)
def _get_avatar_data(item, id_):
data_node = item.getTag('data', namespace=Namespace.AVATAR_DATA)
if data_node is None:
raise MalformedStanzaError('data node missing', item)
data = data_node.getData()
if not data:
raise MalformedStanzaError('data node empty', item)
try:
avatar = b64decode(data, return_type=bytes)
except Exception as error:
raise MalformedStanzaError(f'decoding error: {error}', item)
avatar_sha = hashlib.sha1(avatar).hexdigest()
if avatar_sha != id_:
raise MalformedStanzaError(f'avatar does not match sha', item)
return AvatarData(data=avatar, sha=avatar_sha)
def _make_metadata_node(infos):
item = Node('metadata', attrs={'xmlns': Namespace.AVATAR_METADATA})
for info in infos:
item.addChild('info', attrs=info.to_dict())
return item
def _make_avatar_data_node(avatar):
item = Node('data', attrs={'xmlns': Namespace.AVATAR_DATA})
item.setData(b64encode(avatar.data))
return item
def _get_info_attrs(avatar, avatar_sha, height, width):
info_attrs = {
'id': avatar_sha,
'bytes': len(avatar),
'type': 'image/png',
}
if height is not None:
info_attrs['height'] = height
if width is not None:
info_attrs['width'] = width
return info_attrs
@dataclass
class AvatarInfo:
bytes: int
id: str
type: str
url: str = None
height: int = None
width: int = None
def __post_init__(self):
if self.bytes is None:
raise ValueError
if self.id is None:
raise ValueError
if self.type is None:
raise ValueError
self.bytes = int(self.bytes)
if self.height is not None:
self.height = int(self.height)
if self.width is not None:
self.width = int(self.width)
def to_dict(self):
info_dict = asdict(self)
if self.height is None:
info_dict.pop('height')
if self.width is None:
info_dict.pop('width')
if self.url is None:
info_dict.pop('url')
return info_dict
def __hash__(self):
return hash(self.id)
@dataclass
class AvatarData:
data: bytes
sha: str
@dataclass
class AvatarMetaData:
infos: List[AvatarInfo] = field(default_factory=list)
default: AvatarInfo = None
@classmethod
def from_node(cls, node, default=None):
infos = []
info_nodes = node.getTags('info')
for info in info_nodes:
infos.append(AvatarInfo(
bytes=info.getAttr('bytes'),
id=info.getAttr('id'),
type=info.getAttr('type'),
url=info.getAttr('url'),
height=info.getAttr('height'),
width=info.getAttr('width')
))
return cls(infos=infos, default=default)
def add_avatar_info(self, avatar_info, make_default=False):
self.infos.append(avatar_info)
if make_default:
self.default = avatar_info.id
def to_node(self):
return _make_metadata_node(self.infos)
@property
def avatar_shas(self):
return [info.id for info in self.infos]
@dataclass
class Avatar:
metadata: AvatarMetaData = field(default_factory=AvatarMetaData)
data: Dict[AvatarInfo, bytes] = field(init=False, default_factory=dict)
def add_image_source(self,
data,
type_,
height,
width,
url=None,
make_default=True):
sha = hashlib.sha1(data).hexdigest()
info = AvatarInfo(bytes=len(data),
id=sha,
type=type_,
height=height,
width=width,
url=url)
self.metadata.add_avatar_info(info, make_default=make_default)
self.data[info] = AvatarData(data=data, sha=sha)
def pubsub_avatar_info(self):
for info, data in self.data.items():
if info.url is not None:
continue
yield info, data
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/util.py 0000664 0000000 0000000 00000003365 14130341156 0022152 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from urllib.parse import urlparse
from urllib.parse import unquote
from nbxmpp.structs import CommonResult
from nbxmpp.errors import StanzaError
from nbxmpp.errors import is_error
from nbxmpp.simplexml import Node
def process_response(response):
if response.isError():
raise StanzaError(response)
return CommonResult(jid=response.getFrom())
def raise_if_error(result):
if is_error(result):
raise result
def finalize(task, result):
if is_error(result):
raise result
if isinstance(result, Node):
return task.set_result(result)
return result
def parse_xmpp_uri(uri):
url = urlparse(uri)
if url.scheme != 'xmpp':
raise ValueError('not a xmpp uri')
if not ';' in url.query:
return (url.path, url.query, {})
action, query = url.query.split(';', 1)
key_value_pairs = query.split(';')
dict_ = {}
for key_value in key_value_pairs:
key, value = key_value.split('=')
dict_[key] = unquote(value)
return (url.path, action, dict_)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/vcard4.py 0000664 0000000 0000000 00000076256 14130341156 0022371 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from typing import List
from typing import Optional
from typing import Set
from dataclasses import dataclass
from dataclasses import field
from nbxmpp.simplexml import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.task import iq_request_task
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import raise_if_error
from nbxmpp.modules.util import finalize
log = logging.getLogger('nbxmpp.m.vcard4')
ALLOWED_SEX_VALUES = ['M', 'F', 'O', 'N', 'U']
ALLOWED_KIND_VALUES = ['individual', 'group', 'org', 'location']
# Cardinality
# 1 Exactly one instance per vCard MUST be present.
# *1 Exactly one instance per vCard MAY be present.
# 1* One or more instances per vCard MUST be present.
# * One or more instances per vCard MAY be present.
PROPERTY_DEFINITION = {
'source': (['altid', 'pid', 'pref', 'mediatype'], '*'),
'kind': ([], '*1'),
'fn': (['language', 'altid', 'pid', 'pref', 'type'], '1*'),
'n': (['language', 'altid', 'sort-as'], '*1'),
'nickname': (['language', 'altid', 'pid', 'pref', 'type'], '*'),
'photo': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'bday': (['altid', 'calscale'], '*1'),
'anniversary': (['altid', 'calscale'], '*1'),
'gender': ([], '*1'),
'adr': (['language', 'altid', 'pid', 'pref', 'type', 'geo', 'tz', 'label'], '*'),
'tel': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'email': (['altid', 'pid', 'pref', 'type'], '*'),
'impp': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'lang': (['altid', 'pid', 'pref', 'type'], '*'),
'tz': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'geo': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'title': (['language', 'altid', 'pid', 'pref', 'type'], '*'),
'role': (['language', 'altid', 'pid', 'pref', 'type'], '*'),
'logo': (['language', 'altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'org': (['language', 'altid', 'pid', 'pref', 'type', 'sort-as'], '*'),
'member': (['altid', 'pid', 'pref', 'mediatype'], '*'),
'related': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'categories': (['altid', 'pid', 'pref', 'type'], '*'),
'note': (['language', 'altid', 'pid', 'pref', 'type'], '*'),
'prodid': ([], '*1'),
'rev': ([], '*1'),
'sound': (['language', 'altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'uid': ([], '*1'),
'clientpidmap': ([], '*'),
'url': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'key': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'fburl': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'caladruri': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
'caluri': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'),
}
PROPERTY_VALUE_TYPES = {
'bday': ['date', 'time', 'date-time', 'text'],
'anniversary': ['date', 'time', 'date-time', 'text'],
'key': ['text', 'uri'],
'tel': ['uri', 'text'],
'tz': ['text', 'uri', 'utc-offset'],
'related': ['text', 'uri'],
}
def get_data_from_children(node, child_name):
values = []
child_nodes = node.getTags(child_name)
for child_node in child_nodes:
child_value = child_node.getData()
if child_value:
values.append(child_value)
return values
def add_children(node, child_name, values):
for value in values:
node.addChild(child_name, payload=value)
def get_multiple_type_value(node, types):
for type_ in types:
value = node.getTagData(type_)
if value:
return type_, value
raise ValueError('no value found')
def make_parameters(parameters):
parameters_node = Node('parameters')
for parameter in parameters.values():
parameters_node.addChild(node=parameter.to_node())
return parameters_node
def get_parameters(node):
name = node.getName()
definition = PROPERTY_DEFINITION[name]
allowed_parameters = definition[0]
parameters_node = node.getTag('parameters')
if parameters_node is None:
return Parameters()
parameters = {}
for parameter in allowed_parameters:
parameter_node = parameters_node.getTag(parameter)
if parameter_node is None:
continue
parameter_class = PARAMETER_CLASSES.get(parameter_node.getName())
if parameter_class is None:
continue
parameter = parameter_class.from_node(parameter_node)
parameters[parameter.name] = parameter
return Parameters(parameters)
@dataclass
class Parameter:
name: str
type: str
value: str
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid parameter name: {name}')
value = node.getTagData(cls.type)
if not value:
raise ValueError('no parameter value found')
return cls(value)
def to_node(self):
node = Node(self.name)
node.addChild(self.type, payload=self.value)
return node
def copy(self):
return self.__class__(value=self.value)
@dataclass
class MultiParameter:
name: str
type: str
values: Set[str]
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid parameter name: {name}')
value_nodes = node.getTags(cls.type)
if not value_nodes:
raise ValueError('no parameter value found')
values = set()
for value_node in value_nodes:
value = value_node.getData()
if value:
values.add(value)
if not values:
raise ValueError('no parameter value found')
return cls(values)
def to_node(self):
node = Node(self.name)
for value in self.values:
node.addChild(self.type, payload=value)
return node
def copy(self):
return self.__class__(values=set(self.values))
@dataclass
class LanguageParameter(Parameter):
name: str = field(default='language', init=False)
type: str = field(default='language-tag', init=False)
@dataclass
class PrefParameter(Parameter):
name: str = field(default='pref', init=False)
type: str = field(default='integer', init=False)
@dataclass
class AltidParameter(Parameter):
name: str = field(default='altid', init=False)
type: str = field(default='text', init=False)
@dataclass
class PidParameter(MultiParameter):
name: str = field(default='pid', init=False)
type: str = field(default='text', init=False)
@dataclass
class TypeParameter(MultiParameter):
name: str = field(default='type', init=False)
type: str = field(default='text', init=False)
@dataclass
class MediatypeParameter(Parameter):
name: str = field(default='mediatype', init=False)
type: str = field(default='text', init=False)
@dataclass
class CalscaleParameter(Parameter):
name: str = field(default='calscale', init=False)
type: str = field(default='text', init=False)
@dataclass
class SortasParameter(MultiParameter):
name: str = field(default='sort-as', init=False)
type: str = field(default='text', init=False)
@dataclass
class GeoParameter(Parameter):
name: str = field(default='geo', init=False)
type: str = field(default='uri', init=False)
@dataclass
class TzParameter:
name: str = field(default='tz', init=False)
value_type: str
value: str
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
value_type, value = get_multiple_type_value(node, ['text', 'uri'])
return cls(value_type, value)
def to_node(self):
node = Node(self.name)
node.addChild(self.value_type, payload=self.value)
return node
def copy(self):
return self.__class__(value_type=self.value_type,
value=self.value)
class Parameters:
def __init__(self, parameters=None):
if parameters is None:
parameters = {}
self._parameters = parameters
def values(self):
return self._parameters.values()
def get_types(self):
parameter = self._parameters.get('type')
if parameter is None:
return set()
return parameter.values
def remove_types(self, types):
parameter = self._parameters.get('type')
if parameter is None:
raise ValueError('no type parameter')
for type_ in types:
parameter.values.discard(type_)
if not parameter.values:
self._parameters.pop('type')
def add_types(self, types):
parameter = self._parameters.get('type')
if parameter is None:
parameter = TypeParameter(set(types))
self._parameters['type'] = parameter
return
parameter.values.update(types)
def copy(self):
parameters = {}
for name, parameter in self._parameters.items():
parameters[name] = parameter.copy()
return self.__class__(parameters=parameters)
PARAMETER_CLASSES = {
'language': LanguageParameter,
'pref': PrefParameter,
'altid': AltidParameter,
'pid': PidParameter,
'type': TypeParameter,
'mediatype': MediatypeParameter,
'calscale': CalscaleParameter,
'sort-as': SortasParameter,
'geo': GeoParameter,
'tz': TzParameter
}
@dataclass
class UriProperty:
name: str
value: str
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
value = node.getTagData('uri')
if not value:
raise ValueError('no value found')
parameters = get_parameters(node)
return cls(value, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
node.addChild('uri', payload=self.value)
return node
@property
def is_empty(self):
return not self.value
def copy(self):
return self.__class__(value=self.value,
parameters=self.parameters.copy())
@dataclass
class TextProperty:
name: str
value: str
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
text = node.getTagData('text')
if not text:
raise ValueError('no value found')
parameters = get_parameters(node)
return cls(text, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
node.addChild('text', payload=self.value)
return node
@property
def is_empty(self):
return not self.value
def copy(self):
return self.__class__(value=self.value,
parameters=self.parameters.copy())
@dataclass
class TextListProperty:
name: str
values: List[str]
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
text_nodes = node.getTags('text')
if not text_nodes:
raise ValueError('no value found')
values = get_data_from_children(node, 'text')
if not values:
raise ValueError('no values found')
parameters = get_parameters(node)
return cls(values, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
add_children(node, 'text', self.values)
return node
@property
def is_empty(self):
return not self.values
def copy(self):
return self.__class__(values=list(self.values),
parameters=self.parameters.copy())
@dataclass
class MultipleValueProperty:
name: str
value_type: str
value: str
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
types = PROPERTY_VALUE_TYPES[cls.name]
value_type, value = get_multiple_type_value(node, types)
parameters = get_parameters(node)
return cls(value_type, value, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
node.addChild(self.value_type, payload=self.value)
return node
@property
def is_empty(self):
return not self.value
def copy(self):
return self.__class__(value_type=self.value_type,
value=self.value,
parameters=self.parameters.copy())
@dataclass
class SourceProperty(UriProperty):
name: str = field(default='source', init=False)
@dataclass
class KindProperty(TextProperty):
name: str = field(default='kind', init=False)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
text = node.getTagData('text')
if not text:
raise ValueError('no value found')
if text not in ALLOWED_KIND_VALUES:
text = 'individual'
parameters = get_parameters(node)
return cls(text, parameters)
@dataclass
class FnProperty(TextProperty):
name: str = field(default='fn', init=False)
@dataclass
class NProperty:
name: str = field(default='n', init=False)
surname: List[str] = field(default_factory=list)
given: List[str] = field(default_factory=list)
additional: List[str] = field(default_factory=list)
prefix: List[str] = field(default_factory=list)
suffix: List[str] = field(default_factory=list)
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
surname = get_data_from_children(node, 'surname')
given = get_data_from_children(node, 'given')
additional = get_data_from_children(node, 'additional')
prefix = get_data_from_children(node, 'prefix')
suffix = get_data_from_children(node, 'suffix')
parameters = get_parameters(node)
return cls(surname, given, additional, prefix, suffix, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
add_children(node, 'surname', self.surname)
add_children(node, 'given', self.given)
add_children(node, 'additional', self.additional)
add_children(node, 'prefix', self.prefix)
add_children(node, 'suffix', self.suffix)
return node
@property
def is_empty(self):
if (self.surname or
self.given or
self.additional or
self.suffix or
self.prefix):
return False
return True
def copy(self):
return self.__class__(surname=list(self.surname),
given=list(self.given),
additional=list(self.additional),
prefix=list(self.prefix),
suffix=list(self.suffix),
parameters=self.parameters.copy())
@dataclass
class NicknameProperty(TextListProperty):
name: str = field(default='nickname', init=False)
@dataclass
class PhotoProperty(UriProperty):
name: str = field(default='photo', init=False)
@dataclass
class BDayProperty(MultipleValueProperty):
name: str = field(default='bday', init=False)
@dataclass
class AnniversaryProperty(MultipleValueProperty):
name: str = field(default='anniversary', init=False)
@dataclass
class GenderProperty:
name: str = field(default='gender', init=False)
sex: Optional[str] = None
identity: Optional[str] = None
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
sex = node.getTagData('sex')
if sex not in ALLOWED_SEX_VALUES:
sex = None
identity = node.getTagData('identity')
if not identity:
identity = None
parameters = get_parameters(node)
return cls(sex, identity, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
if self.sex:
node.addChild('sex', payload=self.sex)
if self.identity:
node.addChild('identity', payload=self.identity)
return node
@property
def is_empty(self):
if self.sex or self.identity:
return False
return True
def copy(self):
return self.__class__(sex=self.sex,
identity=self.identity,
parameters=self.parameters.copy())
@dataclass
class AdrProperty:
name: str = field(default='adr', init=False)
pobox: List[str] = field(default_factory=list)
ext: List[str] = field(default_factory=list)
street: List[str] = field(default_factory=list)
locality: List[str] = field(default_factory=list)
region: List[str] = field(default_factory=list)
code: List[str] = field(default_factory=list)
country: List[str] = field(default_factory=list)
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
pobox = get_data_from_children(node, 'pobox')
ext = get_data_from_children(node, 'ext')
street = get_data_from_children(node, 'street')
locality = get_data_from_children(node, 'locality')
region = get_data_from_children(node, 'region')
code = get_data_from_children(node, 'code')
country = get_data_from_children(node, 'country')
parameters = get_parameters(node)
return cls(pobox, ext, street, locality,
region, code, country, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
add_children(node, 'pobox', self.pobox)
add_children(node, 'ext', self.ext)
add_children(node, 'street', self.street)
add_children(node, 'locality', self.locality)
add_children(node, 'region', self.region)
add_children(node, 'code', self.code)
add_children(node, 'country', self.country)
return node
@property
def is_empty(self):
# pylint: disable=too-many-boolean-expressions
if (self.pobox or
self.ext or
self.street or
self.locality or
self.region or
self.code or
self.country):
return False
return True
def copy(self):
return self.__class__(pobox=list(self.pobox),
ext=list(self.ext),
street=list(self.street),
locality=list(self.locality),
region=list(self.region),
code=list(self.code),
country=list(self.country),
parameters=self.parameters.copy())
@dataclass
class TelProperty(MultipleValueProperty):
name: str = field(default='tel', init=False)
@dataclass
class EmailProperty(TextProperty):
name: str = field(default='email', init=False)
@dataclass
class ImppProperty(UriProperty):
name: str = field(default='impp', init=False)
@dataclass
class LangProperty:
name: str = field(default='lang', init=False)
value: str
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
value = node.getTagData('language-tag')
if not value:
raise ValueError('no value found')
parameters = get_parameters(node)
return cls(value, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
node.addChild('language-tag', payload=self.value)
return node
@property
def is_empty(self):
return not self.value
def copy(self):
return self.__class__(value=self.value,
parameters=self.parameters.copy())
@dataclass
class TzProperty(MultipleValueProperty):
name: str = field(default='tz', init=False)
@dataclass
class GeoProperty(UriProperty):
name: str = field(default='geo', init=False)
@dataclass
class TitleProperty(TextProperty):
name: str = field(default='title', init=False)
@dataclass
class RoleProperty(TextProperty):
name: str = field(default='role', init=False)
@dataclass
class LogoProperty(UriProperty):
name: str = field(default='logo', init=False)
@dataclass
class OrgProperty(TextListProperty):
name: str = field(default='org', init=False)
@dataclass
class MemberProperty(UriProperty):
name: str = field(default='member', init=False)
@dataclass
class RelatedProperty(MultipleValueProperty):
name: str = field(default='related', init=False)
@dataclass
class CategoriesProperty(TextListProperty):
name: str = field(default='categories', init=False)
@dataclass
class NoteProperty(TextProperty):
name: str = field(default='note', init=False)
@dataclass
class ProdidProperty(TextProperty):
name: str = field(default='prodid', init=False)
@dataclass
class RevProperty(TextProperty):
name: str = field(default='rev', init=False)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
timestamp = node.getTagData('timestamp')
if not timestamp:
raise ValueError('no value found')
parameters = get_parameters(node)
return cls(timestamp, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
node.addChild('timestamp', payload=self.value)
return node
@dataclass
class SoundProperty(UriProperty):
name: str = field(default='sound', init=False)
@dataclass
class UidProperty(UriProperty):
name: str = field(default='uid', init=False)
@dataclass
class ClientpidmapProperty:
name: str = field(default='clientpidmap', init=False)
sourceid: int
uri: str
parameters: Parameters = field(default_factory=Parameters)
@classmethod
def from_node(cls, node):
name = node.getName()
if name != cls.name:
raise ValueError(f'invalid property name: {name}')
sourceid = node.getTagData('sourceid')
if not sourceid:
raise ValueError('no value found')
uri = node.getTagData('uri')
if not uri:
raise ValueError('no value found')
parameters = get_parameters(node)
return cls(sourceid, uri, parameters)
def to_node(self):
node = Node(self.name)
if self.parameters:
node.addChild(node=make_parameters(self.parameters))
node.addChild('sourceid', payload=self.sourceid)
node.addChild('uri', payload=self.uri)
return node
@property
def is_empty(self):
return not self.uri
def copy(self):
return self.__class__(sourceid=self.sourceid,
uri=self.uri,
parameters=self.parameters.copy())
@dataclass
class UrlProperty(UriProperty):
name: str = field(default='url', init=False)
@dataclass
class KeyProperty(MultipleValueProperty):
name: str = field(default='key', init=False)
@dataclass
class FBurlProperty(UriProperty):
name: str = field(default='fburl', init=False)
@dataclass
class CaladruriProperty(UriProperty):
name: str = field(default='caladruri', init=False)
@dataclass
class CaluriProperty(UriProperty):
name: str = field(default='calurl', init=False)
PROPERTY_CLASSES = {
'source': SourceProperty,
'kind': KindProperty,
'fn': FnProperty,
'n': NProperty,
'nickname': NicknameProperty,
'photo': PhotoProperty,
'bday': BDayProperty,
'anniversary': AnniversaryProperty,
'gender': GenderProperty,
'adr': AdrProperty,
'tel': TelProperty,
'email': EmailProperty,
'impp': ImppProperty,
'lang': LangProperty,
'tz': TzProperty,
'geo': GeoProperty,
'title': TitleProperty,
'role': RoleProperty,
'logo': LogoProperty,
'org': OrgProperty,
'member': MemberProperty,
'related': RelatedProperty,
'categories': CategoriesProperty,
'note': NoteProperty,
'prodid': ProdidProperty,
'rev': RevProperty,
'sound': SoundProperty,
'uid': UidProperty,
'clientpidmap': ClientpidmapProperty,
'url': UrlProperty,
'key': KeyProperty,
'fburl': FBurlProperty,
'caladruri': CaladruriProperty,
'caluri': CaluriProperty,
}
def get_property_from_name(name, node):
property_class = PROPERTY_CLASSES.get(name)
if property_class is None:
return None
try:
return property_class.from_node(node)
except Exception as error:
log.warning('invalid vcard property: %s %s', error, node)
return None
class VCard:
def __init__(self, properties=None):
if properties is None:
properties = []
self._properties = properties
@classmethod
def from_node(cls, node):
properties = []
for child in node.getChildren():
child_name = child.getName()
if child_name == 'group':
group_name = child.getAttr('name')
if not group_name:
continue
group_properties = []
for group_child in child.getChildren():
group_child_name = group_child.getName()
property_ = get_property_from_name(group_child_name,
group_child)
if property_ is None:
continue
group_properties.append(property_)
properties.append((group_name, group_properties))
else:
property_ = get_property_from_name(child_name, child)
if property_ is None:
continue
properties.append((None, property_))
return cls(properties)
def to_node(self):
vcard = Node(f'{Namespace.VCARD4} vcard')
for group, props in self._properties:
if group is None:
vcard.addChild(node=props.to_node())
else:
group = Node(group)
for prop in props:
group.addChild(node=prop.to_node())
vcard.addChild(node=group)
return vcard
def get_properties(self):
properties = []
for group, props in self._properties:
if group is None:
properties.append(props)
else:
properties.extend(props)
return properties
def add_property(self, name, *args, **kwargs):
prop = PROPERTY_CLASSES.get(name)(*args, **kwargs)
self._properties.append((None, prop))
return prop
def remove_property(self, prop):
for _group, props in list(self._properties):
if isinstance(props, list):
if prop in props:
props.remove(prop)
return
elif prop is props:
self._properties.remove((None, props))
return
raise ValueError('prop not found in vcard')
def copy(self):
properties = []
for group_name, props in self._properties:
if group_name is None:
properties.append((None, props.copy()))
else:
group_properties = [prop.copy() for prop in props]
properties.append((group_name, group_properties))
return self.__class__(properties=properties)
class VCard4(BaseModule):
_depends = {
'publish': 'PubSub',
'request_items': 'PubSub',
}
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def request_vcard(self, jid=None):
task = yield
items = yield self.request_items(Namespace.VCARD4_PUBSUB,
jid=jid,
max_items=1)
raise_if_error(items)
if not items:
yield task.set_result(None)
yield _get_vcard(items[0])
@iq_request_task
def set_vcard(self, vcard, public=False):
task = yield
access_model = 'open' if public else 'presence'
options = {
'pubsub#persist_items': 'true',
'pubsub#access_model': access_model,
}
result = yield self.publish(Namespace.VCARD4_PUBSUB,
vcard.to_node(),
id_='current',
options=options,
force_node_options=True)
yield finalize(task, result)
def _get_vcard(item):
vcard = item.getTag('vcard', namespace=Namespace.VCARD4)
if vcard is None:
raise MalformedStanzaError('vcard node missing', item)
try:
vcard = VCard.from_node(vcard)
except Exception as error:
raise MalformedStanzaError('invalid vcard: %s' % error, item)
return vcard
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/vcard_avatar.py 0000664 0000000 0000000 00000004246 14130341156 0023631 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import PresenceType
from nbxmpp.const import AvatarState
from nbxmpp.modules.base import BaseModule
class VCardAvatar(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = [
StanzaHandler(name='presence',
callback=self._process_avatar,
ns=Namespace.VCARD_UPDATE,
priority=15)
]
def _process_avatar(self, _client, stanza, properties):
if properties.type != PresenceType.AVAILABLE:
return
update = stanza.getTag('x', namespace=Namespace.VCARD_UPDATE)
if update is None:
return
avatar_sha = update.getTagData('photo')
if avatar_sha is None:
properties.avatar_state = AvatarState.NOT_READY
self._log.info('%s is not ready to promote an avatar',
stanza.getFrom())
# Empty update element, ignore
return
if avatar_sha == '':
properties.avatar_state = AvatarState.EMPTY
self._log.info('%s empty avatar advertised', stanza.getFrom())
return
properties.avatar_sha = avatar_sha
properties.avatar_state = AvatarState.ADVERTISED
self._log.info('%s advertises %s', stanza.getFrom(), avatar_sha)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/modules/vcard_temp.py 0000664 0000000 0000000 00000010441 14130341156 0023312 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import hashlib
from dataclasses import dataclass
from dataclasses import field
from nbxmpp.task import iq_request_task
from nbxmpp.util import b64decode
from nbxmpp.util import b64encode
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.protocol import Iq
from nbxmpp.simplexml import Node
from nbxmpp.namespaces import Namespace
from nbxmpp.modules.base import BaseModule
from nbxmpp.modules.util import process_response
class VCardTemp(BaseModule):
def __init__(self, client):
BaseModule.__init__(self, client)
self._client = client
self.handlers = []
@iq_request_task
def request_vcard(self, jid=None):
_task = yield
response = yield _make_vcard_request(jid)
if response.isError():
raise StanzaError(response)
vcard_node = _get_vcard_node(response)
yield VCard.from_node(vcard_node)
@iq_request_task
def set_vcard(self, vcard, jid=None):
_task = yield
response = yield _make_vcard_publish(jid, vcard)
yield process_response(response)
def _make_vcard_request(jid):
iq = Iq(typ='get', to=jid)
iq.addChild('vCard', namespace=Namespace.VCARD)
return iq
def _get_vcard_node(response):
vcard_node = response.getTag('vCard', namespace=Namespace.VCARD)
if vcard_node is None:
raise MalformedStanzaError('vCard node missing', response)
return vcard_node
def _make_vcard_publish(jid, vcard):
iq = Iq(typ='set', to=jid)
iq.addChild(node=vcard.to_node())
return iq
@dataclass
class VCard:
data: dict = field(default_factory=dict)
@classmethod
def from_node(cls, node):
dict_ = {}
for info in node.getChildren():
name = info.getName()
if name in ('ADR', 'TEL', 'EMAIL'):
dict_.setdefault(name, [])
entry = {}
for child in info.getChildren():
entry[child.getName()] = child.getData()
dict_[name].append(entry)
elif info.getChildren() == []:
dict_[name] = info.getData()
else:
dict_[name] = {}
for child in info.getChildren():
dict_[name][child.getName()] = child.getData()
return cls(data=dict_)
def to_node(self):
vcard = Node(tag='vCard', attrs={'xmlns': Namespace.VCARD})
for i in self.data:
if i == 'jid':
continue
if isinstance(self.data[i], dict):
child = vcard.addChild(i)
for j in self.data[i]:
child.addChild(j).setData(self.data[i][j])
elif isinstance(self.data[i], list):
for j in self.data[i]:
child = vcard.addChild(i)
for k in j:
child.addChild(k).setData(j[k])
else:
vcard.addChild(i).setData(self.data[i])
return vcard
def set_avatar(self, avatar, type_=None):
avatar = b64encode(avatar)
if 'PHOTO' not in self.data:
self.data['PHOTO'] = {}
self.data['PHOTO']['BINVAL'] = avatar
if type_ is not None:
self.data['PHOTO']['TYPE'] = type_
def get_avatar(self):
try:
avatar = self.data['PHOTO']['BINVAL']
except Exception:
return None, None
if not avatar:
return None, None
avatar = b64decode(avatar, return_type=bytes)
avatar_sha = hashlib.sha1(avatar).hexdigest()
return avatar, avatar_sha
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/namespaces.py 0000664 0000000 0000000 00000020247 14130341156 0021642 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
from dataclasses import dataclass
# pylint: disable=invalid-name
# pylint: disable=line-too-long
@dataclass(frozen=True)
class _Namespaces:
ACTIVITY: str = 'http://jabber.org/protocol/activity'
ADDRESS: str = 'http://jabber.org/protocol/address'
AGENTS: str = 'jabber:iq:agents'
ATTENTION: str = 'urn:xmpp:attention:0'
AUTH: str = 'jabber:iq:auth'
AVATAR_METADATA: str = 'urn:xmpp:avatar:metadata'
AVATAR_DATA: str = 'urn:xmpp:avatar:data'
BIND: str = 'urn:ietf:params:xml:ns:xmpp-bind'
BLOCKING: str = 'urn:xmpp:blocking'
BOB: str = 'urn:xmpp:bob'
BOOKMARKS: str = 'storage:bookmarks'
BOOKMARKS_1: str = 'urn:xmpp:bookmarks:1'
BOOKMARKS_COMPAT: str = 'urn:xmpp:bookmarks:0#compat'
BOOKMARKS_COMPAT_PEP: str = 'urn:xmpp:bookmarks:1#compat-pep'
BOOKMARK_CONVERSION: str = 'urn:xmpp:bookmarks-conversion:0'
BROWSE: str = 'jabber:iq:browse'
BYTESTREAM: str = 'http://jabber.org/protocol/bytestreams'
CAPS: str = 'http://jabber.org/protocol/caps'
CAPTCHA: str = 'urn:xmpp:captcha'
CARBONS: str = 'urn:xmpp:carbons:2'
CHATMARKERS: str = 'urn:xmpp:chat-markers:0'
CHATSTATES: str = 'http://jabber.org/protocol/chatstates'
CLIENT: str = 'jabber:client'
COMMANDS: str = 'http://jabber.org/protocol/commands'
CONFERENCE: str = 'jabber:x:conference'
CORRECT: str = 'urn:xmpp:message-correct:0'
DATA: str = 'jabber:x:data'
DATA_LAYOUT: str = 'http://jabber.org/protocol/xdata-layout'
DATA_MEDIA: str = 'urn:xmpp:media-element'
DATA_VALIDATE: str = 'http://jabber.org/protocol/xdata-validate'
DELAY: str = 'jabber:x:delay'
DELAY2: str = 'urn:xmpp:delay'
DELIMITER: str = 'roster:delimiter'
DISCO: str = 'http://jabber.org/protocol/disco'
DISCO_INFO: str = 'http://jabber.org/protocol/disco#info'
DISCO_ITEMS: str = 'http://jabber.org/protocol/disco#items'
DOMAIN_BASED_NAME: str = 'urn:xmpp:domain-based-name:1'
EME: str = 'urn:xmpp:eme:0'
ENCRYPTED: str = 'jabber:x:encrypted'
FILE_METADATA: str = 'urn:xmpp:file:metadata:0'
FORWARD: str = 'urn:xmpp:forward:0'
FRAMING: str = 'urn:ietf:params:xml:ns:xmpp-framing'
GATEWAY: str = 'jabber:iq:gateway'
GEOLOC: str = 'http://jabber.org/protocol/geoloc'
HASHES: str = 'urn:xmpp:hashes:1'
HASHES_2: str = 'urn:xmpp:hashes:2'
HASHES_BLAKE2B_256: str = 'urn:xmpp:hash-function-text-names:id-blake2b256'
HASHES_BLAKE2B_512: str = 'urn:xmpp:hash-function-text-names:id-blake2b512'
HASHES_MD5: str = 'urn:xmpp:hash-function-text-names:md5'
HASHES_SHA1: str = 'urn:xmpp:hash-function-text-names:sha-1'
HASHES_SHA256: str = 'urn:xmpp:hash-function-text-names:sha-256'
HASHES_SHA512: str = 'urn:xmpp:hash-function-text-names:sha-512'
HASHES_SHA3_256: str = 'urn:xmpp:hash-function-text-names:sha3-256'
HASHES_SHA3_512: str = 'urn:xmpp:hash-function-text-names:sha3-512'
HINTS: str = 'urn:xmpp:hints'
HTTPUPLOAD_0: str = 'urn:xmpp:http:upload:0'
HTTP_AUTH: str = 'http://jabber.org/protocol/http-auth'
IBB: str = 'http://jabber.org/protocol/ibb'
IDLE: str = 'urn:xmpp:idle:1'
JINGLE: str = 'urn:xmpp:jingle:1'
JINGLE_BYTESTREAM: str = 'urn:xmpp:jingle:transports:s5b:1'
JINGLE_DTLS: str = 'urn:xmpp:jingle:apps:dtls:0'
JINGLE_ERRORS: str = 'urn:xmpp:jingle:errors:1'
JINGLE_FILE_TRANSFER: str = 'urn:xmpp:jingle:apps:file-transfer:3'
JINGLE_FILE_TRANSFER_5: str = 'urn:xmpp:jingle:apps:file-transfer:5'
JINGLE_IBB: str = 'urn:xmpp:jingle:transports:ibb:1'
JINGLE_ICE_UDP: str = 'urn:xmpp:jingle:transports:ice-udp:1'
JINGLE_RAW_UDP: str = 'urn:xmpp:jingle:transports:raw-udp:1'
JINGLE_RTP: str = 'urn:xmpp:jingle:apps:rtp:1'
JINGLE_RTP_AUDIO: str = 'urn:xmpp:jingle:apps:rtp:audio'
JINGLE_RTP_VIDEO: str = 'urn:xmpp:jingle:apps:rtp:video'
JINGLE_XTLS: str = 'urn:xmpp:jingle:security:xtls:0'
LAST: str = 'jabber:iq:last'
LOCATION: str = 'http://jabber.org/protocol/geoloc'
MAM_1: str = 'urn:xmpp:mam:1'
MAM_2: str = 'urn:xmpp:mam:2'
MOOD: str = 'http://jabber.org/protocol/mood'
MSG_HINTS: str = 'urn:xmpp:hints'
MUCLUMBUS: str = 'https://xmlns.zombofant.net/muclumbus/search/1.0'
MUC: str = 'http://jabber.org/protocol/muc'
MUC_USER: str = 'http://jabber.org/protocol/muc#user'
MUC_ADMIN: str = 'http://jabber.org/protocol/muc#admin'
MUC_OWNER: str = 'http://jabber.org/protocol/muc#owner'
MUC_UNIQUE: str = 'http://jabber.org/protocol/muc#unique'
MUC_CONFIG: str = 'http://jabber.org/protocol/muc#roomconfig'
MUC_REQUEST: str = 'http://jabber.org/protocol/muc#request'
MUC_INFO: str = 'http://jabber.org/protocol/muc#roominfo'
NICK: str = 'http://jabber.org/protocol/nick'
OMEMO_TEMP: str = 'eu.siacs.conversations.axolotl'
OMEMO_TEMP_BUNDLE: str = 'eu.siacs.conversations.axolotl.bundles'
OMEMO_TEMP_DL: str = 'eu.siacs.conversations.axolotl.devicelist'
OPENPGP: str = 'urn:xmpp:openpgp:0'
OPENPGP_PK: str = 'urn:xmpp:openpgp:0:public-keys'
OPENPGP_SK: str = 'urn:xmpp:openpgp:0:secret-key'
PING: str = 'urn:xmpp:ping'
PRIVACY: str = 'jabber:iq:privacy'
PRIVATE: str = 'jabber:iq:private'
PUBKEY_ATTEST: str = 'urn:xmpp:attest:2'
PUBKEY_PUBKEY: str = 'urn:xmpp:pubkey:2'
PUBKEY_REVOKE: str = 'urn:xmpp:revoke:2'
PUBSUB: str = 'http://jabber.org/protocol/pubsub'
PUBSUB_ERROR: str = 'http://jabber.org/protocol/pubsub#errors'
PUBSUB_CONFIG: str = 'http://jabber.org/protocol/pubsub#node_config'
PUBSUB_EVENT: str = 'http://jabber.org/protocol/pubsub#event'
PUBSUB_OWNER: str = 'http://jabber.org/protocol/pubsub#owner'
PUBSUB_PUBLISH_OPTIONS: str = 'http://jabber.org/protocol/pubsub#publish-options'
PUBSUB_NODE_MAX: str = 'http://jabber.org/protocol/pubsub#config-node-max'
RECEIPTS: str = 'urn:xmpp:receipts'
REGISTER: str = 'jabber:iq:register'
REGISTER_FEATURE: str = 'http://jabber.org/features/iq-register'
REPORTING: str = 'urn:xmpp:reporting:0'
ROSTER: str = 'jabber:iq:roster'
ROSTERNOTES: str = 'storage:rosternotes'
ROSTERX: str = 'http://jabber.org/protocol/rosterx'
ROSTER_VER: str = 'urn:xmpp:features:rosterver'
RSM: str = 'http://jabber.org/protocol/rsm'
SASL: str = 'urn:ietf:params:xml:ns:xmpp-sasl'
SEARCH: str = 'jabber:iq:search'
SECLABEL: str = 'urn:xmpp:sec-label:0'
SECLABEL_CATALOG: str = 'urn:xmpp:sec-label:catalog:2'
SESSION: str = 'urn:ietf:params:xml:ns:xmpp-session'
SFS: str = 'urn:xmpp:sfs:0'
SID: str = 'urn:xmpp:sid:0'
SIGNED: str = 'jabber:x:signed'
SIMS: str = 'urn:xmpp:sims:1'
STANZAS: str = 'urn:ietf:params:xml:ns:xmpp-stanzas'
STICKERS: str = 'urn:xmpp:stickers:0'
STREAM: str = 'http://affinix.com/jabber/stream'
STREAMS: str = 'http://etherx.jabber.org/streams'
STREAM_MGMT: str = 'urn:xmpp:sm:3'
STYLING: str = 'urn:xmpp:styling:0'
TIME_REVISED: str = 'urn:xmpp:time'
TLS: str = 'urn:ietf:params:xml:ns:xmpp-tls'
TUNE: str = 'http://jabber.org/protocol/tune'
URL_DATA: str = 'http://jabber.org/protocol/url-data'
VCARD: str = 'vcard-temp'
VCARD_UPDATE: str = 'vcard-temp:x:update'
VCARD_CONVERSION: str = 'urn:xmpp:pep-vcard-conversion:0'
VCARD4: str = 'urn:ietf:params:xml:ns:vcard-4.0'
VCARD4_PUBSUB: str = 'urn:xmpp:vcard4'
VERSION: str = 'jabber:iq:version'
XHTML_IM: str = 'http://jabber.org/protocol/xhtml-im'
XHTML: str = 'http://www.w3.org/1999/xhtml'
XMPP_STREAMS: str = 'urn:ietf:params:xml:ns:xmpp-streams'
X_OOB: str = 'jabber:x:oob'
Namespace = _Namespaces()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/old_dispatcher.py 0000664 0000000 0000000 00000067772 14130341156 0022525 0 ustar 00root root 0000000 0000000 ## dispatcher.py
##
## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
## modified by Dimitur Kirov
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
"""
Main xmpp decision making logic. Provides library with methods to assign
different handlers to different XMPP stanzas and namespaces
"""
import sys
import re
import uuid
import logging
import inspect
from xml.parsers.expat import ExpatError
from nbxmpp.simplexml import NodeBuilder
from nbxmpp.plugin import PlugIn
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import InvalidFrom
from nbxmpp.protocol import InvalidJid
from nbxmpp.protocol import InvalidStanza
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Presence
from nbxmpp.protocol import Message
from nbxmpp.protocol import Protocol
from nbxmpp.protocol import Node
from nbxmpp.protocol import Error
from nbxmpp.protocol import ERR_FEATURE_NOT_IMPLEMENTED
from nbxmpp.modules.eme import EME
from nbxmpp.modules.http_auth import HTTPAuth
from nbxmpp.modules.presence import BasePresence
from nbxmpp.modules.message import BaseMessage
from nbxmpp.modules.iq import BaseIq
from nbxmpp.modules.nickname import Nickname
from nbxmpp.modules.delay import Delay
from nbxmpp.modules.muc import MUC
from nbxmpp.modules.idle import Idle
from nbxmpp.modules.pgplegacy import PGPLegacy
from nbxmpp.modules.vcard_avatar import VCardAvatar
from nbxmpp.modules.captcha import Captcha
from nbxmpp.modules.entity_caps import EntityCaps
from nbxmpp.modules.blocking import Blocking
from nbxmpp.modules.pubsub import PubSub
from nbxmpp.modules.activity import Activity
from nbxmpp.modules.tune import Tune
from nbxmpp.modules.mood import Mood
from nbxmpp.modules.location import Location
from nbxmpp.modules.user_avatar import UserAvatar
from nbxmpp.modules.openpgp import OpenPGP
from nbxmpp.modules.omemo import OMEMO
from nbxmpp.modules.annotations import Annotations
from nbxmpp.modules.muclumbus import Muclumbus
from nbxmpp.modules.software_version import SoftwareVersion
from nbxmpp.modules.adhoc import AdHoc
from nbxmpp.modules.ibb import IBB
from nbxmpp.modules.discovery import Discovery
from nbxmpp.modules.chat_markers import ChatMarkers
from nbxmpp.modules.receipts import Receipts
from nbxmpp.modules.oob import OOB
from nbxmpp.modules.correction import Correction
from nbxmpp.modules.attention import Attention
from nbxmpp.modules.security_labels import SecurityLabels
from nbxmpp.modules.chatstates import Chatstates
from nbxmpp.modules.register import Register
from nbxmpp.modules.http_upload import HTTPUpload
from nbxmpp.modules.misc import unwrap_carbon
from nbxmpp.modules.misc import unwrap_mam
from nbxmpp.util import get_properties_struct
log = logging.getLogger('nbxmpp.dispatcher')
#: default timeout to wait for response for our id
DEFAULT_TIMEOUT_SECONDS = 25
XML_DECLARATION = ''
# FIXME: ugly
class Dispatcher:
"""
Why is this here - I needed to redefine Dispatcher for BOSH and easiest way
was to inherit original Dispatcher (now renamed to XMPPDispatcher). Trouble
is that reference used to access dispatcher instance is in Client attribute
named by __class__.__name__ of the dispatcher instance .. long story short:
I wrote following to avoid changing each client.Dispatcher.whatever() in
xmpp
If having two kinds of dispatcher will go well, I will rewrite the
dispatcher references in other scripts
"""
def PlugIn(self, client_obj, after_SASL=False, old_features=None):
if client_obj.protocol_type == 'XMPP':
XMPPDispatcher().PlugIn(client_obj)
elif client_obj.protocol_type == 'BOSH':
BOSHDispatcher().PlugIn(client_obj, after_SASL, old_features)
else:
assert False # should never be reached
@classmethod
def get_instance(cls, *args, **kwargs):
"""
Factory Method for object creation
Use this instead of directly initializing the class in order to make
unit testing much easier.
"""
return cls(*args, **kwargs)
class XMPPDispatcher(PlugIn):
"""
Handles XMPP stream and is the first who takes control over a fresh stanza
Is plugged into NonBlockingClient but can be replugged to restart handled
stream headers (used by SASL f.e.).
"""
def __init__(self):
PlugIn.__init__(self)
self.handlers = {}
self._modules = {}
self._expected = {}
self._defaultHandler = None
self._pendingExceptions = []
self._eventHandler = None
self._cycleHandlers = []
self._exported_methods = [
self.RegisterHandler, self.RegisterDefaultHandler,
self.RegisterEventHandler, self.UnregisterCycleHandler,
self.RegisterCycleHandler, self.RegisterHandlerOnce,
self.UnregisterHandler, self.RegisterProtocol,
self.SendAndCallForResponse,
self.getAnID, self.Event, self.send, self.get_module]
# \ufddo -> \ufdef range
c = '\ufdd0'
r = c
while c < '\ufdef':
c = chr(ord(c) + 1)
r += '|' + c
# \ufffe-\uffff, \u1fffe-\u1ffff, ..., \u10fffe-\u10ffff
c = '\ufffe'
r += '|' + c
r += '|' + chr(ord(c) + 1)
while c < '\U0010fffe':
c = chr(ord(c) + 0x10000)
r += '|' + c
r += '|' + chr(ord(c) + 1)
self.invalid_chars_re = re.compile(r)
def getAnID(self):
return str(uuid.uuid4())
def dumpHandlers(self):
"""
Return set of user-registered callbacks in it's internal format. Used
within the library to carry user handlers set over Dispatcher replugins
"""
return self.handlers
def restoreHandlers(self, handlers):
"""
Restore user-registered callbacks structure from dump previously
obtained via dumpHandlers. Used within the library to carry user
handlers set over Dispatcher replugins.
"""
self.handlers = handlers
def get_module(self, name):
return self._modules[name]
def _register_modules(self):
self._modules['BasePresence'] = BasePresence(self._owner)
self._modules['BaseMessage'] = BaseMessage(self._owner)
self._modules['BaseIq'] = BaseIq(self._owner)
self._modules['EME'] = EME(self._owner)
self._modules['HTTPAuth'] = HTTPAuth(self._owner)
self._modules['Nickname'] = Nickname(self._owner)
self._modules['MUC'] = MUC(self._owner)
self._modules['Delay'] = Delay(self._owner)
self._modules['Captcha'] = Captcha(self._owner)
self._modules['Idle'] = Idle(self._owner)
self._modules['PGPLegacy'] = PGPLegacy(self._owner)
self._modules['VCardAvatar'] = VCardAvatar(self._owner)
self._modules['EntityCaps'] = EntityCaps(self._owner)
self._modules['Blocking'] = Blocking(self._owner)
self._modules['PubSub'] = PubSub(self._owner)
self._modules['Mood'] = Mood(self._owner)
self._modules['Activity'] = Activity(self._owner)
self._modules['Tune'] = Tune(self._owner)
self._modules['Location'] = Location(self._owner)
self._modules['UserAvatar'] = UserAvatar(self._owner)
self._modules['OpenPGP'] = OpenPGP(self._owner)
self._modules['OMEMO'] = OMEMO(self._owner)
self._modules['Annotations'] = Annotations(self._owner)
self._modules['Muclumbus'] = Muclumbus(self._owner)
self._modules['SoftwareVersion'] = SoftwareVersion(self._owner)
self._modules['AdHoc'] = AdHoc(self._owner)
self._modules['IBB'] = IBB(self._owner)
self._modules['Discovery'] = Discovery(self._owner)
self._modules['ChatMarkers'] = ChatMarkers(self._owner)
self._modules['Receipts'] = Receipts(self._owner)
self._modules['OOB'] = OOB(self._owner)
self._modules['Correction'] = Correction(self._owner)
self._modules['Attention'] = Attention(self._owner)
self._modules['SecurityLabels'] = SecurityLabels(self._owner)
self._modules['Chatstates'] = Chatstates(self._owner)
self._modules['Register'] = Register(self._owner)
self._modules['HTTPUpload'] = HTTPUpload(self._owner)
for instance in self._modules.values():
for handler in instance.handlers:
self.RegisterHandler(*handler)
def _init(self):
"""
Register default namespaces/protocols/handlers. Used internally
"""
# FIXME: inject dependencies, do not rely that they are defined by our
# owner
self.RegisterNamespace('unknown')
self.RegisterNamespace(Namespace.STREAMS)
self.RegisterNamespace(self._owner.defaultNamespace)
self.RegisterProtocol('iq', Iq)
self.RegisterProtocol('presence', Presence)
self.RegisterProtocol('message', Message)
self.RegisterDefaultHandler(self.returnStanzaHandler)
self.RegisterEventHandler(self._owner._caller._event_dispatcher)
self._register_modules()
def plugin(self, _owner):
"""
Plug the Dispatcher instance into Client class instance and send initial
stream header. Used internally
"""
self._init()
self._owner.lastErrNode = None
self._owner.lastErr = None
self._owner.lastErrCode = None
if hasattr(self._owner, 'StreamInit'):
self._owner.StreamInit()
else:
self.StreamInit()
def plugout(self):
"""
Prepare instance to be destructed
"""
self._modules = {}
self.Stream.dispatch = None
self.Stream.features = None
self.Stream.destroy()
self._owner = None
self.Stream = None
def StreamInit(self):
"""
Send an initial stream header
"""
self._owner.Connection.sendqueue = []
self.Stream = NodeBuilder()
self.Stream.dispatch = self.dispatch
self.Stream._dispatch_depth = 2
self.Stream.stream_header_received = self._check_stream_start
self.Stream.features = None
self._metastream = Node('stream:stream')
self._metastream.setNamespace(self._owner.Namespace)
self._metastream.setAttr('version', '1.0')
self._metastream.setAttr('xmlns:stream', Namespace.STREAMS)
self._metastream.setAttr('to', self._owner.Server)
self._metastream.setAttr('xml:lang', self._owner.lang)
self._owner.send("%s%s>" % (XML_DECLARATION,
str(self._metastream)[:-2]))
def _check_stream_start(self, ns, tag, attrs):
if ns != Namespace.STREAMS or tag != 'stream':
raise ValueError('Incorrect stream start: '
'(%s,%s). Terminating.' % (tag, ns))
def replace_non_character(self, data):
return re.sub(self.invalid_chars_re, '\ufffd', data)
def ProcessNonBlocking(self, data):
"""
Check incoming stream for data waiting
:param data: data received from transports/IO sockets
:return:
1) length of processed data if some data were processed;
2) '0' string if no data were processed but link is alive;
3) 0 (zero) if underlying connection is closed.
"""
# FIXME:
# When an error occurs we disconnect the transport directly. Client's
# disconnect method will never be called.
# Is this intended?
# also look at transports start_disconnect()
data = self.replace_non_character(data)
for handler in self._cycleHandlers:
handler(self)
if len(self._pendingExceptions) > 0:
_pendingException = self._pendingExceptions.pop()
sys.excepthook(*_pendingException)
return None
try:
self.Stream.Parse(data)
# end stream:stream tag received
if self.Stream and self.Stream.has_received_endtag():
self._owner.disconnect(self.Stream.streamError)
return 0
except ExpatError as error:
log.error('Invalid XML received from server. Forcing disconnect.')
log.error(error)
self._owner.Connection.disconnect()
return 0
except ValueError as error:
log.debug('ValueError: %s', error)
self._owner.Connection.pollend()
return 0
if len(self._pendingExceptions) > 0:
_pendingException = self._pendingExceptions.pop()
sys.excepthook(*_pendingException)
return None
if len(data) == 0:
return '0'
return len(data)
def RegisterNamespace(self, xmlns):
"""
Create internal structures for newly registered namespace
You can register handlers for this namespace afterwards. By default
one namespace is already registered
(jabber:client or jabber:component:accept depending on context.
"""
log.debug('Registering namespace "%s"', xmlns)
self.handlers[xmlns] = {}
self.RegisterProtocol('unknown', Protocol, xmlns=xmlns)
self.RegisterProtocol('default', Protocol, xmlns=xmlns)
def RegisterProtocol(self, tag_name, proto, xmlns=None):
"""
Used to declare some top-level stanza name to dispatcher
Needed to start registering handlers for such stanzas. Iq, message and
presence protocols are registered by default.
"""
if not xmlns:
xmlns = self._owner.defaultNamespace
log.debug('Registering protocol "%s" as %s(%s)', tag_name, proto, xmlns)
self.handlers[xmlns][tag_name] = {'type': proto, 'default': []}
def RegisterNamespaceHandler(self, xmlns, handler, typ='', ns='', system=0):
"""
Register handler for processing all stanzas for specified namespace
"""
self.RegisterHandler('default', handler, typ, ns, xmlns, system)
def RegisterHandler(self, name, handler, typ='', ns='', xmlns=None,
system=False, priority=50):
"""
Register user callback as stanzas handler of declared type
Callback arguments:
dispatcher instance (for replying), incoming return of previous
handlers. The callback must raise xmpp.NodeProcessed just before return
if it wants to prevent other callbacks to be called with the same stanza
as argument _and_, more importantly library from returning
stanza to sender with error set.
:param name: name of stanza. F.e. "iq".
:param handler: user callback.
:param typ: value of stanza's "type" attribute. If not specified any
value will match
:param ns: namespace of child that stanza must contain.
:param xmlns: xml namespace
:param system: call handler even if NodeProcessed Exception were raised
already.
:param priority: The priority of the handler, higher get called later
"""
if not xmlns:
xmlns = self._owner.defaultNamespace
if not typ and not ns:
typ = 'default'
log.debug(
'Registering handler %s for "%s" type->%s ns->%s(%s) priority->%s',
handler, name, typ, ns, xmlns, priority)
if xmlns not in self.handlers:
self.RegisterNamespace(xmlns)
if name not in self.handlers[xmlns]:
self.RegisterProtocol(name, Protocol, xmlns)
specific = typ + ns
if specific not in self.handlers[xmlns][name]:
self.handlers[xmlns][name][specific] = []
self.handlers[xmlns][name][specific].append(
{'func': handler,
'system': system,
'priority': priority,
'specific': specific})
def RegisterHandlerOnce(self, name, handler, typ='', ns='', xmlns=None,
system=0):
"""
Unregister handler after first call (not implemented yet)
"""
# FIXME Drop or implement
if not xmlns:
xmlns = self._owner.defaultNamespace
self.RegisterHandler(name, handler, typ, ns, xmlns, system)
def UnregisterHandler(self, name, handler, typ='', ns='', xmlns=None):
"""
Unregister handler. "typ" and "ns" must be specified exactly the same as
with registering.
"""
if not xmlns:
xmlns = self._owner.defaultNamespace
if not typ and not ns:
typ = 'default'
if xmlns not in self.handlers:
return
if name not in self.handlers[xmlns]:
return
specific = typ + ns
if specific not in self.handlers[xmlns][name]:
return
for handler_dict in self.handlers[xmlns][name][specific]:
if handler_dict['func'] == handler:
try:
self.handlers[xmlns][name][specific].remove(handler_dict)
log.debug(
'Unregister handler %s for "%s" type->%s ns->%s(%s)',
handler, name, typ, ns, xmlns)
except ValueError:
log.warning(
'Unregister failed: %s for "%s" type->%s ns->%s(%s)',
handler, name, typ, ns, xmlns)
def RegisterDefaultHandler(self, handler):
"""
Specify the handler that will be used if no NodeProcessed exception were
raised. This is returnStanzaHandler by default.
"""
self._defaultHandler = handler
def RegisterEventHandler(self, handler):
"""
Register handler that will process events. F.e. "FILERECEIVED" event.
See common/connection: _event_dispatcher()
"""
self._eventHandler = handler
def returnStanzaHandler(self, conn, stanza):
"""
Return stanza back to the sender with error
set
"""
if stanza.getType() in ('get', 'set'):
conn._owner.send(Error(stanza, ERR_FEATURE_NOT_IMPLEMENTED))
def RegisterCycleHandler(self, handler):
"""
Register handler that will be called on every Dispatcher.Process() call
"""
if handler not in self._cycleHandlers:
self._cycleHandlers.append(handler)
def UnregisterCycleHandler(self, handler):
"""
Unregister handler that will be called on every Dispatcher.Process()
call
"""
if handler in self._cycleHandlers:
self._cycleHandlers.remove(handler)
def Event(self, realm, event, data=None):
"""
Raise some event
:param realm: scope of event. Usually a namespace.
:param event: the event itself. F.e. "SUCCESSFUL SEND".
:param data: data that comes along with event. Depends on event.
"""
if self._eventHandler:
self._eventHandler(realm, event, data)
else:
log.warning('Received unhandled event: %s', event)
def dispatch(self, stanza):
"""
Main procedure that performs XMPP stanza recognition and calling
apppropriate handlers for it. Called by simplexml
"""
self.Event('', 'STANZA RECEIVED', stanza)
self.Stream._mini_dom = None
# Count stanza
self._owner.Smacks.count_incoming(stanza.getName())
name = stanza.getName()
if name == 'features':
self._owner.got_features = True
self.Stream.features = stanza
elif name == 'error':
if stanza.getTag('see-other-host'):
self._owner.got_see_other_host = stanza
xmlns = stanza.getNamespace()
if xmlns not in self.handlers:
log.warning('Unknown namespace: %s', xmlns)
xmlns = 'unknown'
# features stanza has been handled before
if name not in self.handlers[xmlns]:
if name not in ('features', 'stream'):
log.warning('Unknown stanza: %s', stanza)
else:
log.debug('Got %s / %s stanza', xmlns, name)
name = 'unknown'
else:
log.debug('Got %s / %s stanza', xmlns, name)
# Convert simplexml to Protocol object
try:
stanza = self.handlers[xmlns][name]['type'](node=stanza)
except InvalidJid:
log.warning('Invalid JID, ignoring stanza')
log.warning(stanza)
return
own_jid = self._owner.get_bound_jid()
properties = get_properties_struct(name, own_jid)
if name == 'iq':
if stanza.getFrom() is None and own_jid is not None:
stanza.setFrom(own_jid.bare)
if name == 'message':
# https://tools.ietf.org/html/rfc6120#section-8.1.1.1
# If the stanza does not include a 'to' address then the client MUST
# treat it as if the 'to' address were included with a value of the
# client's full JID.
to = stanza.getTo()
if to is None:
stanza.setTo(own_jid)
elif not to.bare_match(own_jid):
log.warning('Message addressed to someone else: %s', stanza)
return
if stanza.getFrom() is None:
stanza.setFrom(own_jid.bare)
# Unwrap carbon
try:
stanza, properties.carbon = unwrap_carbon(stanza, own_jid)
except (InvalidFrom, InvalidJid) as exc:
log.warning(exc)
log.warning(stanza)
return
except NodeProcessed as exc:
log.info(exc)
return
# Unwrap mam
try:
stanza, properties.mam = unwrap_mam(stanza, own_jid)
except (InvalidStanza, InvalidJid) as exc:
log.warning(exc)
log.warning(stanza)
return
typ = stanza.getType()
if name == 'message' and not typ:
typ = 'normal'
elif not typ:
typ = ''
stanza.props = stanza.getProperties()
log.debug('type: %s, properties: %s', typ, stanza.props)
_id = stanza.getID()
processed = False
if _id in self._expected:
cb, args = self._expected[_id]
log.debug('Expected stanza arrived. Callback %s(%s) found',
cb, args)
try:
if args is None:
cb(self, stanza)
else:
cb(self, stanza, **args)
except NodeProcessed:
pass
return
# Gather specifics depending on stanza properties
specifics = ['default']
if typ and typ in self.handlers[xmlns][name]:
specifics.append(typ)
for prop in stanza.props:
if prop in self.handlers[xmlns][name]:
specifics.append(prop)
if typ and typ + prop in self.handlers[xmlns][name]:
specifics.append(typ + prop)
# Create the handler chain
chain = []
chain += self.handlers[xmlns]['default']['default']
for specific in specifics:
chain += self.handlers[xmlns][name][specific]
# Sort chain with priority
chain.sort(key=lambda x: x['priority'])
for handler in chain:
if not processed or handler['system']:
try:
log.info('Call handler: %s', handler['func'].__qualname__)
# Backwards compatibility until all handlers support
# properties
signature = inspect.signature(handler['func'])
if len(signature.parameters) > 2:
handler['func'](self, stanza, properties)
else:
handler['func'](self, stanza)
except NodeProcessed:
processed = True
except Exception:
self._pendingExceptions.insert(0, sys.exc_info())
return
# Stanza was not processed call default handler
if not processed and self._defaultHandler:
self._defaultHandler(self, stanza)
def SendAndCallForResponse(self, stanza, func=None, args=None):
"""
Put stanza on the wire and call back when recipient replies. Additional
callback arguments can be specified in args
"""
_waitid = self.send(stanza)
self._expected[_waitid] = (func, args)
return _waitid
def send(self, stanza, now=False):
"""
Wrap transports send method when plugged into NonBlockingClient. Makes
sure stanzas get ID and from tag.
"""
ID = None
if isinstance(stanza, Protocol):
ID = stanza.getID()
if ID is None:
stanza.setID(self.getAnID())
ID = stanza.getID()
if self._owner._registered_name and not stanza.getAttr('from'):
stanza.setAttr('from', self._owner._registered_name)
self._owner.Connection.send(stanza, now)
# If no ID then it is a whitespace
if hasattr(self._owner, 'Smacks') and ID:
self._owner.Smacks.save_in_queue(stanza)
return ID
class BOSHDispatcher(XMPPDispatcher):
def PlugIn(self, owner, after_SASL=False, old_features=None):
self.old_features = old_features
self.after_SASL = after_SASL
XMPPDispatcher.PlugIn(self, owner)
def StreamInit(self):
"""
Send an initial stream header
"""
self.Stream = NodeBuilder()
self.Stream.dispatch = self.dispatch
self.Stream._dispatch_depth = 2
self.Stream.stream_header_received = self._check_stream_start
self.Stream.features = self.old_features
self._metastream = Node('stream:stream')
self._metastream.setNamespace(self._owner.Namespace)
self._metastream.setAttr('version', '1.0')
self._metastream.setAttr('xmlns:stream', Namespace.STREAMS)
self._metastream.setAttr('to', self._owner.Server)
self._metastream.setAttr('xml:lang', self._owner.lang)
self.restart = True
self._owner.Connection.send_init(after_SASL=self.after_SASL)
def StreamTerminate(self):
"""
Send a stream terminator
"""
self._owner.Connection.send_terminator()
def ProcessNonBlocking(self, data=None):
if self.restart:
fromstream = self._metastream
fromstream.setAttr('from', fromstream.getAttr('to'))
fromstream.delAttr('to')
data = '%s%s>%s' % (XML_DECLARATION, str(fromstream)[:-2], data)
self.restart = False
return XMPPDispatcher.ProcessNonBlocking(self, data)
def dispatch(self, stanza):
if stanza.getName() == 'body' and stanza.getNamespace() == Namespace.HTTP_BIND:
stanza_attrs = stanza.getAttrs()
if 'authid' in stanza_attrs:
# should be only in init response
# auth module expects id of stream in document attributes
self.Stream._document_attrs['id'] = stanza_attrs['authid']
self._owner.Connection.handle_body_attrs(stanza_attrs)
children = stanza.getChildren()
if children:
for child in children:
# if child doesn't have any ns specified, simplexml
# (or expat) thinks it's of parent's (BOSH body) namespace,
# so we have to rewrite it to jabber:client
if child.getNamespace() == Namespace.HTTP_BIND:
child.setNamespace(self._owner.defaultNamespace)
XMPPDispatcher.dispatch(self, child)
else:
XMPPDispatcher.dispatch(self, stanza)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/plugin.py 0000664 0000000 0000000 00000006735 14130341156 0021027 0 ustar 00root root 0000000 0000000 ## plugin.py
##
## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
"""
Provides PlugIn class functionality to develop extentions for xmpppy
"""
import logging
log = logging.getLogger('nbxmpp.plugin')
class PlugIn:
"""
Abstract xmpppy plugin infrastructure code, providing plugging in/out and
debugging functionality
Inherit to develop pluggable objects. No code change on the owner class
required (the object where we plug into)
For every instance of PlugIn class the 'owner' is the class in what the plug
was plugged.
"""
def __init__(self):
self._exported_methods=[]
def PlugIn(self, owner, *args, **kwargs):
"""
Attach to owner and register ourself and our _exported_methods in it.
If defined by a subclass, call self.plugin(owner) to execute hook
code after plugging
"""
self._owner=owner
log.info('Plugging %s __INTO__ %s', self, self._owner)
if self.__class__.__name__ in owner.__dict__:
log.debug('Plugging ignored: another instance already plugged.')
return None
self._old_owners_methods=[]
for method in self._exported_methods:
if method.__name__ in owner.__dict__:
self._old_owners_methods.append(owner.__dict__[method.__name__])
owner.__dict__[method.__name__]=method
if self.__class__.__name__.endswith('Dispatcher'):
# FIXME: I need BOSHDispatcher or XMPPDispatcher on .Dispatcher
# there must be a better way..
owner.__dict__['Dispatcher']=self
else:
owner.__dict__[self.__class__.__name__]=self
# Execute hook
if hasattr(self, 'plugin'):
return self.plugin(owner, *args, **kwargs)
return None
def PlugOut(self, *args, **kwargs):
"""
Unregister our _exported_methods from owner and detach from it.
If defined by a subclass, call self.plugout() after unplugging to
execute hook code
"""
log.info('Plugging %s __OUT__ of %s.', self, self._owner)
for method in self._exported_methods:
del self._owner.__dict__[method.__name__]
for method in self._old_owners_methods:
self._owner.__dict__[method.__name__]=method
# FIXME: Dispatcher workaround
if self.__class__.__name__.endswith('Dispatcher'):
del self._owner.__dict__['Dispatcher']
else:
del self._owner.__dict__[self.__class__.__name__]
# Execute hook
if hasattr(self, 'plugout'):
return self.plugout(*args, **kwargs)
del self._owner
return None
@classmethod
def get_instance(cls, *args, **kwargs):
"""
Factory Method for object creation
Use this instead of directly initializing the class in order to make
unit testing easier. For testing, this method can be patched to inject
mock objects.
"""
return cls(*args, **kwargs)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/protocol.py 0000664 0000000 0000000 00000217030 14130341156 0021362 0 ustar 00root root 0000000 0000000 ## protocol.py
##
## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
"""
Protocol module contains tools that are needed for processing of xmpp-related
data structures, including jabber-objects like JID or different stanzas and
sub- stanzas) handling routines
"""
import time
import hashlib
import functools
import warnings
from base64 import b64encode
from collections import namedtuple
from gi.repository import GLib
import idna
from precis_i18n import get_profile
from nbxmpp.simplexml import Node
from nbxmpp.namespaces import Namespace
def ascii_upper(s):
return s.upper()
SASL_AUTH_MECHS = [
'SCRAM-SHA-256-PLUS',
'SCRAM-SHA-256',
'SCRAM-SHA-1-PLUS',
'SCRAM-SHA-1',
'GSSAPI',
'PLAIN',
'EXTERNAL',
'ANONYMOUS',
]
SASL_ERROR_CONDITIONS = [
'aborted',
'account-disabled',
'credentials-expired',
'encryption-required',
'incorrect-encoding',
'invalid-authzid',
'invalid-mechanism',
'mechanism-too-weak',
'malformed-request',
'not-authorized',
'temporary-auth-failure',
]
ERRORS = {
'urn:ietf:params:xml:ns:xmpp-sasl aborted': ['',
'',
'The receiving entity acknowledges an element sent by the initiating entity; sent in reply to the element.'],
'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding': ['',
'',
'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 element or an element with initial response data.'],
'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid': ['',
'',
'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 element or an element with initial response data.'],
'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism': ['',
'',
'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 element.'],
'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak': ['',
'',
'The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a element or an element with initial response data.'],
'urn:ietf:params:xml:ns:xmpp-sasl not-authorized': ['',
'',
'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 element or an element with initial response data.'],
'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure': ['',
'',
'The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an element or element.'],
'urn:ietf:params:xml:ns:xmpp-stanzas bad-request': ['400',
'modify',
'The sender has sent XML that is malformed or that cannot be processed.'],
'urn:ietf:params:xml:ns:xmpp-stanzas conflict': ['409',
'cancel',
'Access cannot be granted because an existing resource or session exists with the same name or address.'],
'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented': ['501',
'cancel',
'The feature requested is not implemented by the recipient or server and therefore cannot be processed.'],
'urn:ietf:params:xml:ns:xmpp-stanzas forbidden': ['403',
'auth',
'The requesting entity does not possess the required permissions to perform the action.'],
'urn:ietf:params:xml:ns:xmpp-stanzas gone': ['302',
'modify',
'The recipient or server can no longer be contacted at this address.'],
'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error': ['500',
'wait',
'The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.'],
'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found': ['404',
'cancel',
'The addressed JID or item requested cannot be found.'],
'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed': ['400',
'modify',
"The value of the 'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme."],
'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable': ['406',
'cancel',
'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.'],
'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed': ['405',
'cancel',
'The recipient or server does not allow any entity to perform the action.'],
'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized': ['401',
'auth',
'The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.'],
'urn:ietf:params:xml:ns:xmpp-stanzas payment-required': ['402',
'auth',
'The requesting entity is not authorized to access the requested service because payment is required.'],
'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable': ['404',
'wait',
'The intended recipient is temporarily unavailable.'],
'urn:ietf:params:xml:ns:xmpp-stanzas redirect': ['302',
'modify',
'The recipient or server is redirecting requests for this information to another entity.'],
'urn:ietf:params:xml:ns:xmpp-stanzas registration-required': ['407',
'auth',
'The requesting entity is not authorized to access the requested service because registration is required.'],
'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found': ['404',
'cancel',
'A remote server or service specified as part or all of the JID of the intended recipient does not exist.'],
'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout': ['504',
'wait',
'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.'],
'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint': ['500',
'wait',
'The server or recipient lacks the system resources necessary to service the request.'],
'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable': ['503',
'cancel',
'The server or recipient does not currently provide the requested service.'],
'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required': ['407',
'auth',
'The requesting entity is not authorized to access the requested service because a subscription is required.'],
'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition': ['500',
'',
'Undefined Condition'],
'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request': ['400',
'wait',
'The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).'],
'urn:ietf:params:xml:ns:xmpp-streams bad-format': ['',
'',
'The entity has sent XML that cannot be processed.'],
'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix': ['',
'',
'The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.'],
'urn:ietf:params:xml:ns:xmpp-streams conflict': ['',
'',
'The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.'],
'urn:ietf:params:xml:ns:xmpp-streams connection-timeout': ['',
'',
'The entity has not generated any traffic over the stream for some period of time.'],
'urn:ietf:params:xml:ns:xmpp-streams host-gone': ['',
'',
"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."],
'urn:ietf:params:xml:ns:xmpp-streams host-unknown': ['',
'',
"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."],
'urn:ietf:params:xml:ns:xmpp-streams improper-addressing': ['',
'',
"A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value)."],
'urn:ietf:params:xml:ns:xmpp-streams internal-server-error': ['',
'',
'The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.'],
'urn:ietf:params:xml:ns:xmpp-streams invalid-from': ['cancel',
'',
"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."],
'urn:ietf:params:xml:ns:xmpp-streams invalid-id': ['',
'',
'The stream ID or dialback ID is invalid or does not match an ID previously provided.'],
'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace': ['',
'',
'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".'],
'urn:ietf:params:xml:ns:xmpp-streams invalid-xml': ['',
'',
'The entity has sent invalid XML over the stream to a server that performs validation.'],
'urn:ietf:params:xml:ns:xmpp-streams not-authorized': ['',
'',
'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.'],
'urn:ietf:params:xml:ns:xmpp-streams policy-violation': ['',
'',
'The entity has violated some local service policy.'],
'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed': ['',
'',
'The server is unable to properly connect to a remote resource that is required for authentication or authorization.'],
'urn:ietf:params:xml:ns:xmpp-streams resource-constraint': ['',
'',
'The server lacks the system resources necessary to service the stream.'],
'urn:ietf:params:xml:ns:xmpp-streams restricted-xml': ['',
'',
'The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.'],
'urn:ietf:params:xml:ns:xmpp-streams see-other-host': ['',
'',
'The server will not provide service to the initiating entity but is redirecting traffic to another host.'],
'urn:ietf:params:xml:ns:xmpp-streams system-shutdown': ['',
'',
'The server is being shut down and all active streams are being closed.'],
'urn:ietf:params:xml:ns:xmpp-streams undefined-condition': ['',
'',
'The error condition is not one of those defined by the other conditions in this list.'],
'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding': ['',
'',
'The initiating entity has encoded the stream in an encoding that is not supported by the server.'],
'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type': ['',
'',
'The initiating entity has sent a first-level child of the stream that is not supported by the server.'],
'urn:ietf:params:xml:ns:xmpp-streams unsupported-version': ['',
'',
"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."],
'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed': ['',
'',
'The initiating entity has sent XML that is not well-formed.']
}
_errorcodes = {
'302': 'redirect',
'400': 'unexpected-request',
'401': 'not-authorized',
'402': 'payment-required',
'403': 'forbidden',
'404': 'remote-server-not-found',
'405': 'not-allowed',
'406': 'not-acceptable',
'407': 'subscription-required',
'409': 'conflict',
'500': 'undefined-condition',
'501': 'feature-not-implemented',
'503': 'service-unavailable',
'504': 'remote-server-timeout',
'cancel': 'invalid-from'
}
_status_conditions = {
'realjid-public': 100,
'affiliation-changed': 101,
'unavailable-shown': 102,
'unavailable-not-shown': 103,
'configuration-changed': 104,
'self-presence': 110,
'logging-enabled': 170,
'logging-disabled': 171,
'non-anonymous': 172,
'semi-anonymous': 173,
'fully-anonymous': 174,
'room-created': 201,
'nick-assigned': 210,
'banned': 301,
'new-nick': 303,
'kicked': 307,
'removed-affiliation': 321,
'removed-membership': 322,
'removed-shutdown': 332,
}
_localpart_disallowed_chars = set('"&\'/:<>@')
_localpart_escape_chars = ' "&\'/:<>@'
STREAM_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-streams not-authorized'
STREAM_REMOTE_CONNECTION_FAILED = 'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed'
SASL_MECHANISM_TOO_WEAK = 'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak'
STREAM_XML_NOT_WELL_FORMED = 'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed'
ERR_JID_MALFORMED = 'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed'
STREAM_SEE_OTHER_HOST = 'urn:ietf:params:xml:ns:xmpp-streams see-other-host'
STREAM_BAD_NAMESPACE_PREFIX = 'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix'
ERR_SERVICE_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable'
STREAM_CONNECTION_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-streams connection-timeout'
STREAM_UNSUPPORTED_VERSION = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-version'
STREAM_IMPROPER_ADDRESSING = 'urn:ietf:params:xml:ns:xmpp-streams improper-addressing'
STREAM_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-streams undefined-condition'
SASL_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-sasl not-authorized'
ERR_GONE = 'urn:ietf:params:xml:ns:xmpp-stanzas gone'
SASL_TEMPORARY_AUTH_FAILURE = 'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure'
ERR_REMOTE_SERVER_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found'
ERR_UNEXPECTED_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request'
ERR_RECIPIENT_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable'
ERR_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-stanzas conflict'
STREAM_SYSTEM_SHUTDOWN = 'urn:ietf:params:xml:ns:xmpp-streams system-shutdown'
STREAM_BAD_FORMAT = 'urn:ietf:params:xml:ns:xmpp-streams bad-format'
ERR_SUBSCRIPTION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required'
STREAM_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-streams internal-server-error'
ERR_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized'
SASL_ABORTED = 'urn:ietf:params:xml:ns:xmpp-sasl aborted'
ERR_REGISTRATION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas registration-required'
ERR_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error'
SASL_INCORRECT_ENCODING = 'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding'
STREAM_HOST_GONE = 'urn:ietf:params:xml:ns:xmpp-streams host-gone'
STREAM_POLICY_VIOLATION = 'urn:ietf:params:xml:ns:xmpp-streams policy-violation'
STREAM_INVALID_XML = 'urn:ietf:params:xml:ns:xmpp-streams invalid-xml'
STREAM_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-streams conflict'
STREAM_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-streams resource-constraint'
STREAM_UNSUPPORTED_ENCODING = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding'
ERR_NOT_ALLOWED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed'
ERR_ITEM_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found'
ERR_NOT_ACCEPTABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable'
STREAM_INVALID_FROM = 'urn:ietf:params:xml:ns:xmpp-streams invalid-from'
ERR_FEATURE_NOT_IMPLEMENTED = 'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented'
ERR_BAD_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas bad-request'
STREAM_INVALID_ID = 'urn:ietf:params:xml:ns:xmpp-streams invalid-id'
STREAM_HOST_UNKNOWN = 'urn:ietf:params:xml:ns:xmpp-streams host-unknown'
ERR_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition'
SASL_INVALID_MECHANISM = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism'
STREAM_RESTRICTED_XML = 'urn:ietf:params:xml:ns:xmpp-streams restricted-xml'
ERR_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint'
ERR_REMOTE_SERVER_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout'
SASL_INVALID_AUTHZID = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid'
ERR_PAYMENT_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas payment-required'
STREAM_INVALID_NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace'
ERR_REDIRECT = 'urn:ietf:params:xml:ns:xmpp-stanzas redirect'
STREAM_UNSUPPORTED_STANZA_TYPE = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type'
ERR_FORBIDDEN = 'urn:ietf:params:xml:ns:xmpp-stanzas forbidden'
def isResultNode(node):
"""
Return true if the node is a positive reply
"""
return node and node.getType() == 'result'
def isErrorNode(node):
"""
Return true if the node is a negative reply
"""
return node and node.getType() == 'error'
def isMucPM(message):
muc_user = message.getTag('x', namespace=Namespace.MUC_USER)
return (message.getType() in ('chat', 'error') and
muc_user is not None and
not muc_user.getChildren())
class NodeProcessed(Exception):
"""
Exception that should be raised by handler when the handling should be
stopped
"""
class StreamError(Exception):
"""
Base exception class for stream errors
"""
class BadFormat(StreamError):
pass
class BadNamespacePrefix(StreamError):
pass
class Conflict(StreamError):
pass
class ConnectionTimeout(StreamError):
pass
class HostGone(StreamError):
pass
class HostUnknown(StreamError):
pass
class ImproperAddressing(StreamError):
pass
class InternalServerError(StreamError):
pass
class InvalidFrom(StreamError):
pass
class InvalidID(StreamError):
pass
class InvalidNamespace(StreamError):
pass
class InvalidXML(StreamError):
pass
class NotAuthorized(StreamError):
pass
class PolicyViolation(StreamError):
pass
class RemoteConnectionFailed(StreamError):
pass
class ResourceConstraint(StreamError):
pass
class RestrictedXML(StreamError):
pass
class SeeOtherHost(StreamError):
pass
class SystemShutdown(StreamError):
pass
class UndefinedCondition(StreamError):
pass
class UnsupportedEncoding(StreamError):
pass
class UnsupportedStanzaType(StreamError):
pass
class UnsupportedVersion(StreamError):
pass
class XMLNotWellFormed(StreamError):
pass
class InvalidStanza(Exception):
pass
class InvalidJid(Exception):
pass
class LocalpartByteLimit(InvalidJid):
def __init__(self):
InvalidJid.__init__(self, 'Localpart must be between 1 and 1023 bytes')
class LocalpartNotAllowedChar(InvalidJid):
def __init__(self):
InvalidJid.__init__(self, 'Not allowed character in localpart')
class ResourcepartByteLimit(InvalidJid):
def __init__(self):
InvalidJid.__init__(self,
'Resourcepart must be between 1 and 1023 bytes')
class ResourcepartNotAllowedChar(InvalidJid):
def __init__(self):
InvalidJid.__init__(self, 'Not allowed character in resourcepart')
class DomainpartByteLimit(InvalidJid):
def __init__(self):
InvalidJid.__init__(self, 'Domainpart must be between 1 and 1023 bytes')
class DomainpartNotAllowedChar(InvalidJid):
def __init__(self):
InvalidJid.__init__(self, 'Not allowed character in domainpart')
class StanzaMalformed(Exception):
pass
class DiscoInfoMalformed(Exception):
pass
stream_exceptions = {'bad-format': BadFormat,
'bad-namespace-prefix': BadNamespacePrefix,
'conflict': Conflict,
'connection-timeout': ConnectionTimeout,
'host-gone': HostGone,
'host-unknown': HostUnknown,
'improper-addressing': ImproperAddressing,
'internal-server-error': InternalServerError,
'invalid-from': InvalidFrom,
'invalid-id': InvalidID,
'invalid-namespace': InvalidNamespace,
'invalid-xml': InvalidXML,
'not-authorized': NotAuthorized,
'policy-violation': PolicyViolation,
'remote-connection-failed': RemoteConnectionFailed,
'resource-constraint': ResourceConstraint,
'restricted-xml': RestrictedXML,
'see-other-host': SeeOtherHost,
'system-shutdown': SystemShutdown,
'undefined-condition': UndefinedCondition,
'unsupported-encoding': UnsupportedEncoding,
'unsupported-stanza-type': UnsupportedStanzaType,
'unsupported-version': UnsupportedVersion,
'xml-not-well-formed': XMLNotWellFormed}
def deprecation_warning(message):
warnings.warn(message, DeprecationWarning)
@functools.lru_cache(maxsize=None)
def validate_localpart(localpart):
if not localpart or len(localpart.encode()) > 1023:
raise LocalpartByteLimit
if _localpart_disallowed_chars & set(localpart):
raise LocalpartNotAllowedChar
try:
username = get_profile('UsernameCaseMapped')
return username.enforce(localpart)
except Exception:
raise LocalpartNotAllowedChar
@functools.lru_cache(maxsize=None)
def validate_resourcepart(resourcepart):
if not resourcepart or len(resourcepart.encode()) > 1023:
raise ResourcepartByteLimit
try:
opaque = get_profile('OpaqueString')
return opaque.enforce(resourcepart)
except Exception:
raise ResourcepartNotAllowedChar
@functools.lru_cache(maxsize=None)
def validate_domainpart(domainpart):
if not domainpart:
raise DomainpartByteLimit
ip_address = domainpart.strip('[]')
if GLib.hostname_is_ip_address(ip_address):
return ip_address
length = len(domainpart.encode())
if length == 0 or length > 1023:
raise DomainpartByteLimit
if domainpart.endswith('.'): # RFC7622, 3.2
domainpart = domainpart[:-1]
try:
idna_encode(domainpart)
except Exception:
raise DomainpartNotAllowedChar
return domainpart
@functools.lru_cache(maxsize=None)
def idna_encode(domain):
return idna.encode(domain, uts46=True).decode()
@functools.lru_cache(maxsize=None)
def escape_localpart(localpart):
# https://xmpp.org/extensions/xep-0106.html#bizrules-algorithm
#
# If there are any instances of character sequences that correspond
# to escapings of the disallowed characters
# (e.g., the character sequence "\27") or the escaping character
# (i.e., the character sequence "\5c") in the source address,
# the leading backslash character MUST be escaped to the character
# sequence "\5c"
for char in '\\' + _localpart_escape_chars:
seq = "\\{:02x}".format(ord(char))
localpart = localpart.replace(seq, "\\5c{:02x}".format(ord(char)))
# Escape all other chars
for char in _localpart_escape_chars:
localpart = localpart.replace(char, "\\{:02x}".format(ord(char)))
return localpart
@functools.lru_cache(maxsize=None)
def unescape_localpart(localpart):
if localpart.startswith('\\20') or localpart.endswith('\\20'):
# Escaped JIDs are not allowed to start or end with \20
# so this localpart must be already unescaped
return localpart
for char in _localpart_escape_chars:
seq = "\\{:02x}".format(ord(char))
localpart = localpart.replace(seq, char)
for char in _localpart_escape_chars + "\\":
seq = "\\5c{:02x}".format(ord(char))
localpart = localpart.replace(seq, "\\{:02x}".format(ord(char)))
return localpart
class JID(namedtuple('JID',
['jid', 'localpart', 'domain', 'resource'])):
__slots__ = []
def __new__(cls, jid=None, localpart=None, domain=None, resource=None):
if jid is not None:
deprecation_warning('JID(jid) is deprecated, use from_string()')
return JID.from_string(str(jid))
if localpart is not None:
localpart = validate_localpart(localpart)
domain = validate_domainpart(domain)
if resource is not None:
resource = validate_resourcepart(resource)
return super().__new__(cls, None, localpart, domain, resource)
@classmethod
@functools.lru_cache(maxsize=None)
def from_string(cls, jid_string):
# https://tools.ietf.org/html/rfc7622#section-3.2
# Remove any portion from the first '/' character to the end of the
# string (if there is a '/' character present).
# Remove any portion from the beginning of the string to the first
# '@' character (if there is an '@' character present).
if jid_string.find('/') != -1:
rest, resourcepart = jid_string.split('/', 1)
else:
rest, resourcepart = jid_string, None
if rest.find('@') != -1:
localpart, domainpart = rest.split('@', 1)
else:
localpart, domainpart = None, rest
return cls(jid=None,
localpart=localpart,
domain=domainpart,
resource=resourcepart)
@classmethod
@functools.lru_cache(maxsize=None)
def from_user_input(cls, user_input, escaped=False):
# Use this if we want JIDs to be escaped according to XEP-0106
# The standard JID parsing cannot be applied because user_input is
# not a valid JID.
# Only user_input which after escaping result in a bare JID can be
# successfully parsed.
# The assumpution is user_input is a bare JID so we start with an
# rsplit on @ because we assume there is no resource, so the char @
# in the localpart can later be escaped.
if escaped:
# for convenience
return cls.from_string(user_input)
if '@' in user_input:
localpart, domainpart = user_input.rsplit('@', 1)
if localpart.startswith(' ') or localpart.endswith(' '):
raise LocalpartNotAllowedChar
localpart = escape_localpart(localpart)
else:
localpart = None
domainpart = user_input
return cls(jid=None,
localpart=localpart,
domain=domainpart,
resource=None)
def __str__(self):
if self.localpart:
jid = f'{self.localpart}@{self.domain}'
else:
jid = self.domain
if self.resource is not None:
return f'{jid}/{self.resource}'
return jid
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
if isinstance(other, str):
deprecation_warning('comparing string with JID is deprected')
try:
return JID.from_string(other) == self
except Exception:
return False
return super().__eq__(other)
def __ne__(self, other):
return not self.__eq__(other)
def domain_to_ascii(self):
return idna_encode(self.domain)
@property
def bare(self):
if self.localpart is not None:
return f'{self.localpart}@{self.domain}'
return self.domain
@property
def is_bare(self):
return self.resource is None
def new_as_bare(self):
if self.resource is None:
return self
return self._replace(resource=None)
def bare_match(self, other):
if isinstance(other, str):
other = JID.from_string(other)
return self.bare == other.bare
@property
def is_domain(self):
return self.localpart is None and self.resource is None
@property
def is_full(self):
return (self.localpart is not None and
self.domain is not None and
self.resource is not None)
def new_with(self, **kwargs):
return self._replace(**kwargs)
def to_user_string(self, show_punycode=True):
domain = self.domain_to_ascii()
if domain.startswith('xn--') and show_punycode:
domain_encoded = f' ({domain})'
else:
domain_encoded = ''
if self.localpart is None:
return f'{self}{domain_encoded}'
localpart = unescape_localpart(self.localpart)
if self.resource is None:
return f'{localpart}@{self.domain}{domain_encoded}'
return f'{localpart}@{self.domain}/{self.resource}{domain_encoded}'
def bareMatch(self, other):
deprecation_warning('bareMatch() is deprected use bare_match()')
return self.bare_match(other)
@property
def isBare(self):
deprecation_warning('isBare() is deprected use '
'the attribute is_bare')
return self.is_bare
@property
def isDomain(self):
deprecation_warning('isDomain() is deprected use '
'the attribute is_domain')
return self.is_domain
@property
def isFull(self):
deprecation_warning('isFull() is deprected use '
'the attribute is_full')
return self.is_full
def copy(self):
deprecation_warning('copy() is not needed, JID is immutable')
return self
def getNode(self):
deprecation_warning('getNode() is deprected use '
'the attribute localpart')
return self.localpart
def getDomain(self):
deprecation_warning('getDomain() is deprected use '
'the attribute domain')
return self.domain
def getResource(self):
deprecation_warning('getResource() is deprected use '
'the attribute resource')
return self.resource
def getStripped(self):
deprecation_warning('getStripped() is deprected use '
'the attribute bare')
return self.bare
def getBare(self):
deprecation_warning('getBare() is deprected use '
'the attribute bare')
return self.bare
class StreamErrorNode(Node):
def __init__(self, node):
Node.__init__(self, node=node)
self._text = {}
text_elements = self.getTags('text', namespace=Namespace.XMPP_STREAMS)
for element in text_elements:
lang = element.getXmlLang()
text = element.getData()
self._text[lang] = text
def get_condition(self):
for tag in self.getChildren():
if (tag.getName() != 'text' and
tag.getNamespace() == Namespace.XMPP_STREAMS):
return tag.getName()
return None
def get_text(self, pref_lang=None):
if pref_lang is not None:
text = self._text.get(pref_lang)
if text is not None:
return text
if self._text:
text = self._text.get('en')
if text is not None:
return text
text = self._text.get(None)
if text is not None:
return text
return self._text.popitem()[1]
return ''
class Protocol(Node):
"""
A "stanza" object class. Contains methods that are common for presences, iqs
and messages
"""
def __init__(self,
name=None,
to=None,
typ=None,
frm=None,
attrs=None,
payload=None,
timestamp=None,
xmlns=None,
node=None):
"""
Constructor, name is the name of the stanza
i.e. 'message' or 'presence'or 'iq'
to is the value of 'to' attribure, 'typ' - 'type' attribute
frn - from attribure, attrs - other attributes mapping,
payload - same meaning as for simplexml payload definition
timestamp - the time value that needs to be stamped over stanza
xmlns - namespace of top stanza node
node - parsed or unparsed stana to be taken as prototype.
"""
if not attrs:
attrs = {}
if to:
attrs['to'] = to
if frm:
attrs['from'] = frm
if typ:
attrs['type'] = typ
Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
if not node and xmlns:
self.setNamespace(xmlns)
if self['to']:
self.setTo(self['to'])
if self['from']:
self.setFrom(self['from'])
if (node and
isinstance(node, Protocol) and
self.__class__ == node.__class__
and 'id' in self.attrs):
del self.attrs['id']
self.timestamp = None
for d in self.getTags('delay', namespace=Namespace.DELAY2):
try:
if d.getAttr('stamp') < self.getTimestamp2():
self.setTimestamp(d.getAttr('stamp'))
except Exception:
pass
if not self.timestamp:
for x in self.getTags('x', namespace=Namespace.DELAY):
try:
if x.getAttr('stamp') < self.getTimestamp():
self.setTimestamp(x.getAttr('stamp'))
except Exception:
pass
if timestamp is not None:
self.setTimestamp(timestamp)
def isError(self):
return self.getAttr('type') == 'error'
def isResult(self):
return self.getAttr('type') == 'result'
def getTo(self):
"""
Return value of the 'to' attribute
"""
try:
return self['to']
except Exception:
pass
return None
def getFrom(self):
"""
Return value of the 'from' attribute
"""
try:
return self['from']
except Exception:
pass
return None
def getTimestamp(self):
"""
Return the timestamp in the 'yyyymmddThhmmss' format
"""
if self.timestamp:
return self.timestamp
return time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
def getTimestamp2(self):
"""
Return the timestamp in the 'yyyymmddThhmmss' format
"""
if self.timestamp:
return self.timestamp
return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def getJid(self):
"""
Return the value of the 'jid' attribute
"""
attr = self.getAttr('jid')
if attr:
return JID.from_string(attr)
return attr
def getID(self):
"""
Return the value of the 'id' attribute
"""
return self.getAttr('id')
def setTo(self, val):
"""
Set the value of the 'to' attribute
"""
if not isinstance(val, JID):
val = JID.from_string(val)
self.setAttr('to', val)
def getType(self):
"""
Return the value of the 'type' attribute
"""
return self.getAttr('type')
def setFrom(self, val):
"""
Set the value of the 'from' attribute
"""
if not isinstance(val, JID):
val = JID.from_string(val)
self.setAttr('from', val)
def setType(self, val):
"""
Set the value of the 'type' attribute
"""
self.setAttr('type', val)
def setID(self, val):
"""
Set the value of the 'id' attribute
"""
self.setAttr('id', val)
def getError(self):
"""
Return the error-condition (if present) or the textual description
of the error (otherwise)
"""
errtag = self.getTag('error')
if errtag is None:
return None
for tag in errtag.getChildren():
if (tag.getName() != 'text' and
tag.getNamespace() == Namespace.STANZAS):
return tag.getName()
return None
def getAppError(self):
errtag = self.getTag('error')
if errtag is None:
return None
for tag in errtag.getChildren():
if (tag.getName() != 'text' and
tag.getNamespace() != Namespace.STANZAS):
return tag.getName()
return None
def getAppErrorNamespace(self):
errtag = self.getTag('error')
if errtag is None:
return None
for tag in errtag.getChildren():
if (tag.getName() != 'text' and
tag.getNamespace() != Namespace.STANZAS):
return tag.getNamespace()
return None
def getErrorMsg(self):
"""
Return the textual description of the error (if present)
or the error condition
"""
errtag = self.getTag('error')
if errtag:
for tag in errtag.getChildren():
if tag.getName() == 'text':
return tag.getData()
return self.getError()
return None
def getErrorCode(self):
"""
Return the error code. Obsolete.
"""
return self.getTagAttr('error', 'code')
def getErrorType(self):
"""
Return the error code. Obsolete.
"""
return self.getTagAttr('error', 'type')
def getStatusConditions(self, as_code=False):
"""
Return the status conditions list as defined in XEP-0306.
"""
result = set()
status_tags = self.getTags('status')
for status in status_tags:
if as_code:
code = status.getAttr('code')
if code is not None:
result.add(code)
else:
for condition in status.getChildren():
result.add(condition.getName())
return list(result)
def setError(self, error, code=None):
"""
Set the error code. Obsolete. Use error-conditions instead
"""
if code:
if str(code) in _errorcodes.keys():
error = ErrorNode(_errorcodes[str(code)], text=error)
else:
error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code,
typ='cancel', text=error)
elif isinstance(error, str):
error = ErrorNode(error)
self.setType('error')
self.addChild(node=error)
def setTimestamp(self, val=None):
"""
Set the timestamp. timestamp should be the yyyymmddThhmmss string
"""
if not val:
val = time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
self.timestamp=val
self.setTag('x', {'stamp': self.timestamp}, namespace=Namespace.DELAY)
def getProperties(self):
"""
Return the list of namespaces to which belongs the
direct childs of element
"""
props = []
for child in self.getChildren():
prop = child.getNamespace()
if prop not in props:
props.append(prop)
return props
def getTag(self, name, attrs=None, namespace=None, protocol=False):
"""
Return the Node instance for the tag.
If protocol is True convert to a new Protocol/Message instance.
"""
tag = Node.getTag(self, name, attrs, namespace)
if protocol and tag:
if name == 'message':
return Message(node=tag)
return Protocol(node=tag)
return tag
def __setitem__(self, item, val):
"""
Set the item 'item' to the value 'val'
"""
if item in ['to', 'from']:
if not isinstance(val, JID):
val = JID.from_string(val)
return self.setAttr(item, val)
class Message(Protocol):
"""
XMPP Message stanza - "push" mechanism
"""
def __init__(self,
to=None,
body=None,
xhtml=None,
typ=None,
subject=None,
attrs=None,
frm=None,
payload=None,
timestamp=None,
xmlns=Namespace.CLIENT,
node=None):
"""
You can specify recipient, text of message, type of message any
additional attributes, sender of the message, any additional payload
(f.e. jabber:x:delay element) and namespace in one go.
Alternatively you can pass in the other XML object as the 'node'
parameted to replicate it as message
"""
Protocol.__init__(self,
'message',
to=to,
typ=typ,
attrs=attrs,
frm=frm,
payload=payload,
timestamp=timestamp,
xmlns=xmlns,
node=node)
if body:
self.setBody(body)
if xhtml is not None:
self.setXHTML(xhtml)
if subject is not None:
self.setSubject(subject)
def getBody(self):
"""
Return text of the message
"""
return self.getTagData('body')
def getXHTML(self):
return self.getTag('html', namespace=Namespace.XHTML_IM)
def getSubject(self):
"""
Return subject of the message
"""
return self.getTagData('subject')
def getThread(self):
"""
Return thread of the message
"""
return self.getTagData('thread')
def getOriginID(self):
"""
Return origin-id of the message
"""
return self.getTagAttr('origin-id', namespace=Namespace.SID, attr='id')
def getStanzaIDAttrs(self):
"""
Return the stanza-id attributes of the message
"""
try:
attrs = self.getTag('stanza-id', namespace=Namespace.SID).getAttrs()
except Exception:
return None, None
return attrs['id'], attrs['by']
def setBody(self, val):
"""
Set the text of the message"""
self.setTagData('body', val)
def setXHTML(self, body, add=False):
if isinstance(body, str):
body = Node(node=body)
if add:
xhtml = self.getTag('html', namespace=Namespace.XHTML_IM)
if xhtml is not None:
xhtml.addChild(node=body)
else:
self.addChild('html',
namespace=Namespace.XHTML_IM,
payload=body)
else:
xhtml_nodes = self.getTags('html', namespace=Namespace.XHTML_IM)
for xhtml in xhtml_nodes:
self.delChild(xhtml)
self.addChild('html', namespace=Namespace.XHTML_IM, payload=body)
def setSubject(self, val):
"""
Set the subject of the message
"""
self.setTagData('subject', val)
def setThread(self, val):
"""
Set the thread of the message
"""
self.setTagData('thread', val)
def setOriginID(self, val):
"""
Sets the origin-id of the message
"""
self.setTag('origin-id', namespace=Namespace.SID, attrs={'id': val})
def buildReply(self, text=None):
"""
Builds and returns another message object with specified text. The to,
from, thread and type properties of new message are pre-set as reply to
this message
"""
m = Message(to=self.getFrom(),
frm=self.getTo(),
body=text,
typ=self.getType())
th = self.getThread()
if th:
m.setThread(th)
return m
def getStatusCode(self):
"""
Return the status code of the message (for groupchat config change)
"""
attrs = []
for xtag in self.getTags('x'):
for child in xtag.getTags('status'):
attrs.append(child.getAttr('code'))
return attrs
def setMarker(self, type_, id_):
self.setTag(type_, namespace=Namespace.CHATMARKERS, attrs={'id': id_})
def setMarkable(self):
self.setTag('markable', namespace=Namespace.CHATMARKERS)
def setReceiptRequest(self):
self.setTag('request', namespace=Namespace.RECEIPTS)
def setReceiptReceived(self, id_):
self.setTag('received', namespace=Namespace.RECEIPTS, attrs={'id': id_})
def setOOB(self, url, desc=None):
oob = self.setTag('x', namespace=Namespace.X_OOB)
oob.setTagData('url', url)
if desc is not None:
oob.setTagData('desc', desc)
def setCorrection(self, id_):
self.setTag('replace', namespace=Namespace.CORRECT, attrs={'id': id_})
def setAttention(self):
self.setTag('attention', namespace=Namespace.ATTENTION)
def setHint(self, hint):
self.setTag(hint, namespace=Namespace.HINTS)
class Presence(Protocol):
def __init__(self,
to=None,
typ=None,
priority=None,
show=None,
status=None,
attrs=None,
frm=None,
timestamp=None,
payload=None,
xmlns=Namespace.CLIENT,
node=None):
"""
You can specify recipient, type of message, priority, show and status
values any additional attributes, sender of the presence, timestamp, any
additional payload (f.e. jabber:x:delay element) and namespace in one
go. Alternatively you can pass in the other XML object as the 'node'
parameted to replicate it as presence
"""
Protocol.__init__(self,
'presence',
to=to,
typ=typ,
attrs=attrs,
frm=frm,
payload=payload,
timestamp=timestamp,
xmlns=xmlns,
node=node)
if priority:
self.setPriority(priority)
if show:
self.setShow(show)
if status:
self.setStatus(status)
def getPriority(self):
"""
Return the priority of the message
"""
return self.getTagData('priority')
def getShow(self):
"""
Return the show value of the message
"""
return self.getTagData('show')
def getStatus(self):
"""
Return the status string of the message
"""
return self.getTagData('status') or ''
def setPriority(self, val):
"""
Set the priority of the message
"""
self.setTagData('priority', val)
def setShow(self, val):
"""
Set the show value of the message
"""
if val not in ['away', 'chat', 'dnd', 'xa']:
raise ValueError('Invalid show value: %s' % val)
self.setTagData('show', val)
def setStatus(self, val):
"""
Set the status string of the message
"""
self.setTagData('status', val)
def _muc_getItemAttr(self, tag, attr):
for xtag in self.getTags('x'):
if xtag.getNamespace() not in (Namespace.MUC_USER,
Namespace.MUC_ADMIN):
continue
for child in xtag.getTags(tag):
return child.getAttr(attr)
def _muc_getSubTagDataAttr(self, tag, attr):
for xtag in self.getTags('x'):
if xtag.getNamespace() not in (Namespace.MUC_USER,
Namespace.MUC_ADMIN):
continue
for child in xtag.getTags('item'):
for cchild in child.getTags(tag):
return cchild.getData(), cchild.getAttr(attr)
return None, None
def getRole(self):
"""
Return the presence role (for groupchat)
"""
return self._muc_getItemAttr('item', 'role')
def getAffiliation(self):
"""
Return the presence affiliation (for groupchat)
"""
return self._muc_getItemAttr('item', 'affiliation')
def getNewNick(self):
"""
Return the status code of the presence (for groupchat)
"""
return self._muc_getItemAttr('item', 'nick')
def getJid(self):
"""
Return the presence jid (for groupchat)
"""
return self._muc_getItemAttr('item', 'jid')
def getReason(self):
"""
Returns the reason of the presence (for groupchat)
"""
return self._muc_getSubTagDataAttr('reason', '')[0]
def getActor(self):
"""
Return the reason of the presence (for groupchat)
"""
return self._muc_getSubTagDataAttr('actor', 'jid')[1]
def getStatusCode(self):
"""
Return the status code of the presence (for groupchat)
"""
attrs = []
for xtag in self.getTags('x'):
for child in xtag.getTags('status'):
attrs.append(child.getAttr('code'))
return attrs
class Iq(Protocol):
"""
XMPP Iq object - get/set dialog mechanism
"""
def __init__(self,
typ=None,
queryNS=None,
attrs=None,
to=None,
frm=None,
payload=None,
xmlns=Namespace.CLIENT,
node=None):
"""
You can specify type, query namespace any additional attributes,
recipient of the iq, sender of the iq, any additional payload (f.e.
jabber:x:data node) and namespace in one go.
Alternatively you can pass in the other XML object as the 'node'
parameted to replicate it as an iq
"""
Protocol.__init__(self,
'iq',
to=to,
typ=typ,
attrs=attrs,
frm=frm,
xmlns=xmlns,
node=node)
if payload:
self.setQueryPayload(payload)
if queryNS:
self.setQueryNS(queryNS)
def getQuery(self):
"""
Return the IQ's child element if it exists, None otherwise.
"""
children = self.getChildren()
if children and self.getType() != 'error' and \
children[0].getName() != 'error':
return children[0]
return None
def getQueryNS(self):
"""
Return the namespace of the 'query' child element
"""
tag = self.getQuery()
if tag:
return tag.getNamespace()
return None
def getQuerynode(self):
"""
Return the 'node' attribute value of the 'query' child element
"""
tag = self.getQuery()
if tag:
return tag.getAttr('node')
return None
def getQueryPayload(self):
"""
Return the 'query' child element payload
"""
tag = self.getQuery()
if tag:
return tag.getPayload()
return None
def getQueryChildren(self):
"""
Return the 'query' child element child nodes
"""
tag = self.getQuery()
if tag:
return tag.getChildren()
return None
def getQueryChild(self, name=None):
"""
Return the 'query' child element with name, or the first element
which is not an error element
"""
query = self.getQuery()
if not query:
return None
for node in query.getChildren():
if name is not None:
if node.getName() == name:
return node
else:
if node.getName() != 'error':
return node
return None
def setQuery(self, name=None):
"""
Change the name of the query node, creating it if needed. Keep the
existing name if none is given (use 'query' if it's a creation).
Return the query node.
"""
query = self.getQuery()
if query is None:
query = self.addChild('query')
if name is not None:
query.setName(name)
return query
def setQueryNS(self, namespace):
"""
Set the namespace of the 'query' child element
"""
self.setQuery().setNamespace(namespace)
def setQueryPayload(self, payload):
"""
Set the 'query' child element payload
"""
self.setQuery().setPayload(payload)
def setQuerynode(self, node):
"""
Set the 'node' attribute value of the 'query' child element
"""
self.setQuery().setAttr('node', node)
def buildReply(self, typ):
"""
Build and return another Iq object of specified type. The to, from and
query child node of new Iq are pre-set as reply to this Iq.
"""
iq = Iq(typ,
to=self.getFrom(),
frm=self.getTo(),
attrs={'id': self.getID()})
iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
return iq
def buildSimpleReply(self, typ):
return Iq(typ,
to=self.getFrom(),
attrs={'id': self.getID()})
class Hashes(Node):
"""
Hash elements for various XEPs as defined in XEP-300
RECOMENDED HASH USE:
Algorithm Support
MD2 MUST NOT
MD4 MUST NOT
MD5 MAY
SHA-1 MUST
SHA-256 MUST
SHA-512 SHOULD
"""
supported = ('md5', 'sha-1', 'sha-256', 'sha-512')
def __init__(self, nsp=Namespace.HASHES):
Node.__init__(self, None, {}, [], None, None, False, None)
self.setNamespace(nsp)
self.setName('hash')
def calculateHash(self, algo, file_string):
"""
Calculate the hash and add it. It is preferable doing it here
instead of doing it all over the place in Gajim.
"""
hl = None
hash_ = None
# file_string can be a string or a file
if isinstance(file_string, str):
if algo == 'sha-1':
hl = hashlib.sha1()
elif algo == 'md5':
hl = hashlib.md5()
elif algo == 'sha-256':
hl = hashlib.sha256()
elif algo == 'sha-512':
hl = hashlib.sha512()
if hl:
hl.update(file_string)
hash_ = hl.hexdigest()
else: # if it is a file
if algo == 'sha-1':
hl = hashlib.sha1()
elif algo == 'md5':
hl = hashlib.md5()
elif algo == 'sha-256':
hl = hashlib.sha256()
elif algo == 'sha-512':
hl = hashlib.sha512()
if hl:
for line in file_string:
hl.update(line)
hash_ = hl.hexdigest()
return hash_
def addHash(self, hash_, algo):
self.setAttr('algo', algo)
self.setData(hash_)
class Hashes2(Node):
"""
Hash elements for various XEPs as defined in XEP-300
RECOMENDED HASH USE:
Algorithm Support
MD2 MUST NOT
MD4 MUST NOT
MD5 MUST NOT
SHA-1 SHOULD NOT
SHA-256 MUST
SHA-512 SHOULD
SHA3-256 MUST
SHA3-512 SHOULD
BLAKE2b256 MUST
BLAKE2b512 SHOULD
"""
supported = ('sha-256', 'sha-512', 'sha3-256',
'sha3-512', 'blake2b-256', 'blake2b-512')
def __init__(self, nsp=Namespace.HASHES_2):
Node.__init__(self, None, {}, [], None, None, False, None)
self.setNamespace(nsp)
self.setName('hash')
def calculateHash(self, algo, file_string):
"""
Calculate the hash and add it. It is preferable doing it here
instead of doing it all over the place in Gajim.
"""
hl = None
hash_ = None
if algo == 'sha-256':
hl = hashlib.sha256()
elif algo == 'sha-512':
hl = hashlib.sha512()
elif algo == 'sha3-256':
hl = hashlib.sha3_256()
elif algo == 'sha3-512':
hl = hashlib.sha3_512()
elif algo == 'blake2b-256':
hl = hashlib.blake2b(digest_size=32)
elif algo == 'blake2b-512':
hl = hashlib.blake2b(digest_size=64)
# file_string can be a string or a file
if hl is not None:
if isinstance(file_string, bytes):
hl.update(file_string)
else: # if it is a file
for line in file_string:
hl.update(line)
hash_ = b64encode(hl.digest()).decode('ascii')
return hash_
def addHash(self, hash_, algo):
self.setAttr('algo', algo)
self.setData(hash_)
class BindRequest(Iq):
def __init__(self, resource):
if resource is not None:
resource = Node('resource', payload=resource)
Iq.__init__(self, typ='set')
self.addChild(node=Node('bind',
{'xmlns': Namespace.BIND},
payload=resource))
class TLSRequest(Node):
def __init__(self):
Node.__init__(self, tag='starttls', attrs={'xmlns': Namespace.TLS})
class SessionRequest(Iq):
def __init__(self):
Iq.__init__(self, typ='set')
self.addChild(node=Node('session', attrs={'xmlns': Namespace.SESSION}))
class StreamHeader(Node):
def __init__(self, domain, lang=None):
if lang is None:
lang = 'en'
Node.__init__(self,
tag='stream:stream',
attrs={'xmlns': Namespace.CLIENT,
'version': '1.0',
'xmlns:stream': Namespace.STREAMS,
'to': domain,
'xml:lang': lang})
class WebsocketOpenHeader(Node):
def __init__(self, domain, lang=None):
if lang is None:
lang = 'en'
Node.__init__(self,
tag='open',
attrs={'xmlns': Namespace.FRAMING,
'version': '1.0',
'to': domain,
'xml:lang': lang})
class WebsocketCloseHeader(Node):
def __init__(self):
Node.__init__(self, tag='close', attrs={'xmlns': Namespace.FRAMING})
class Features(Node):
def __init__(self, node):
Node.__init__(self, node=node)
def has_starttls(self):
tls = self.getTag('starttls', namespace=Namespace.TLS)
if tls is not None:
required = tls.getTag('required') is not None
return True, required
return False, False
def has_sasl(self):
return self.getTag('mechanisms', namespace=Namespace.SASL) is not None
def get_mechs(self):
mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL)
if mechanisms is None:
return set()
mechanisms = mechanisms.getTags('mechanism')
return set(mech.getData() for mech in mechanisms)
def get_domain_based_name(self):
hostname = self.getTag('hostname',
namespace=Namespace.DOMAIN_BASED_NAME)
if hostname is not None:
return hostname.getData()
return None
def has_bind(self):
return self.getTag('bind', namespace=Namespace.BIND) is not None
def session_required(self):
session = self.getTag('session', namespace=Namespace.SESSION)
if session is not None:
optional = session.getTag('optional') is not None
return not optional
return False
def has_sm(self):
return self.getTag('sm', namespace=Namespace.STREAM_MGMT) is not None
def has_roster_version(self):
return self.getTag('ver', namespace=Namespace.ROSTER_VER) is not None
def has_register(self):
return self.getTag(
'register', namespace=Namespace.REGISTER_FEATURE) is not None
def has_anonymous(self):
return 'ANONYMOUS' in self.get_mechs()
class ErrorNode(Node):
"""
XMPP-style error element
In the case of stanza error should be attached to XMPP stanza.
In the case of stream-level errors should be used separately.
"""
def __init__(self, name, code=None, typ=None, text=None):
"""
Mandatory parameter: name - name of error condition.
Optional parameters: code, typ, text.
Used for backwards compartibility with older jabber protocol.
"""
if name in ERRORS:
cod, type_, txt = ERRORS[name]
ns = name.split()[0]
else:
cod, ns, type_, txt = '500', Namespace.STANZAS, 'cancel', ''
if typ:
type_ = typ
if code:
cod = code
if text:
txt = text
Node.__init__(self, 'error', {}, [Node(name)])
if type_:
self.setAttr('type', type_)
if not cod:
self.setName('stream:error')
if txt:
self.addChild(node=Node(ns + ' text', {}, [txt]))
if cod:
self.setAttr('code', cod)
class Error(Protocol):
"""
Used to quickly transform received stanza into error reply
"""
def __init__(self, node, error, reply=1):
"""
Create error reply basing on the received 'node' stanza and the 'error'
error condition
If the 'node' is not the received stanza but locally created ('to' and
'from' fields needs not swapping) specify the 'reply' argument as false.
"""
if reply:
Protocol.__init__(self,
to=node.getFrom(),
frm=node.getTo(),
node=node)
else:
Protocol.__init__(self, node=node)
self.setError(error)
if node.getType() == 'error':
self.__str__ = self.__dupstr__
def __dupstr__(self, _dup1=None, _dup2=None):
"""
Dummy function used as preventor of creating error node in reply to
error node. I.e. you will not be able to serialise "double" error
into string.
"""
return ''
class DataField(Node):
"""
This class is used in the DataForm class to describe the single data item
If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) then
you will need to work with instances of this class.
"""
def __init__(self,
name=None,
value=None,
typ=None,
required=0,
desc=None,
options=None,
node=None):
"""
Create new data field of specified name,value and type
Also 'required','desc' and 'options' fields can be set. Alternatively
other XML object can be passed in as the 'node' parameted
to replicate it as a new datafiled.
"""
Node.__init__(self, 'field', node=node)
if name:
self.setVar(name)
if isinstance(value, (list, tuple)):
self.setValues(value)
elif value:
self.setValue(value)
if typ:
self.setType(typ)
elif not typ and not node:
self.setType('text-single')
if required:
self.setRequired(required)
if desc:
self.setDesc(desc)
if options:
self.setOptions(options)
def setRequired(self, req=1):
"""
Change the state of the 'required' flag
"""
if req:
self.setTag('required')
else:
try:
self.delChild('required')
except ValueError:
return
def isRequired(self):
"""
Return in this field a required one
"""
return self.getTag('required')
def setDesc(self, desc):
"""
Set the description of this field
"""
self.setTagData('desc', desc)
def getDesc(self):
"""
Return the description of this field
"""
return self.getTagData('desc')
def setValue(self, val):
"""
Set the value of this field
"""
self.setTagData('value', val)
def getValue(self):
return self.getTagData('value')
def setValues(self, lst):
"""
Set the values of this field as values-list. Replaces all previous filed
values! If you need to just add a value - use addValue method
"""
while self.getTag('value'):
self.delChild('value')
for val in lst:
self.addValue(val)
def addValue(self, val):
"""
Add one more value to this field. Used in 'get' iq's or such
"""
self.addChild('value', {}, [val])
def getValues(self):
"""
Return the list of values associated with this field
"""
ret = []
for tag in self.getTags('value'):
ret.append(tag.getData())
return ret
def getOptions(self):
"""
Return label-option pairs list associated with this field
"""
ret = []
for tag in self.getTags('option'):
ret.append([tag.getAttr('label'), tag.getTagData('value')])
return ret
def setOptions(self, lst):
"""
Set label-option pairs list associated with this field
"""
while self.getTag('option'):
self.delChild('option')
for opt in lst:
self.addOption(opt)
def addOption(self, opt):
"""
Add one more label-option pair to this field
"""
if isinstance(opt, list):
self.addChild('option',
{'label': opt[0]}).setTagData('value', opt[1])
else:
self.addChild('option').setTagData('value', opt)
def getType(self):
"""
Get type of this field
"""
return self.getAttr('type')
def setType(self, val):
"""
Set type of this field
"""
return self.setAttr('type', val)
def getVar(self):
"""
Get 'var' attribute value of this field
"""
return self.getAttr('var')
def setVar(self, val):
"""
Set 'var' attribute value of this field
"""
return self.setAttr('var', val)
class DataForm(Node):
"""
Used for manipulating dataforms in XMPP
Relevant XEPs: 0004, 0068, 0122. Can be used in disco, pub-sub and many
other applications.
"""
def __init__(self, typ=None, data=None, title=None, node=None):
"""
Create new dataform of type 'typ'. 'data' is the list of DataField
instances that this dataform contains, 'title' - the title string. You
can specify the 'node' argument as the other node to be used as base for
constructing this dataform
title and instructions is optional and SHOULD NOT contain newlines.
Several instructions MAY be present.
'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' )
'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' )
respectively.
'cancel' form can not contain any fields. All other forms contains
AT LEAST one field.
'title' MAY be included in forms of type "form" and "result"
"""
Node.__init__(self, 'x', node=node)
if node:
newkids = []
for n in self.getChildren():
if n.getName() == 'field':
newkids.append(DataField(node=n))
else:
newkids.append(n)
self.kids = newkids
if typ:
self.setType(typ)
self.setNamespace(Namespace.DATA)
if title:
self.setTitle(title)
if data is not None:
if isinstance(data, dict):
newdata = []
for name in data.keys():
newdata.append(DataField(name, data[name]))
data = newdata
for child in data:
if child.__class__.__name__ == 'DataField':
self.kids.append(child)
elif isinstance(child, Node):
self.kids.append(DataField(node=child))
else: # Must be a string
self.addInstructions(child)
def getType(self):
"""
Return the type of dataform
"""
return self.getAttr('type')
def setType(self, typ):
"""
Set the type of dataform
"""
self.setAttr('type', typ)
def getTitle(self):
"""
Return the title of dataform
"""
return self.getTagData('title')
def setTitle(self, text):
"""
Set the title of dataform
"""
self.setTagData('title', text)
def getInstructions(self):
"""
Return the instructions of dataform
"""
return self.getTagData('instructions')
def setInstructions(self, text):
"""
Set the instructions of dataform
"""
self.setTagData('instructions', text)
def addInstructions(self, text):
"""
Add one more instruction to the dataform
"""
self.addChild('instructions', {}, [text])
def getField(self, name):
"""
Return the datafield object with name 'name' (if exists)
"""
return self.getTag('field', attrs={'var': name})
def setField(self, name):
"""
Create if nessessary or get the existing datafield object with name
'name' and return it
"""
f = self.getField(name)
if f:
return f
return self.addChild(node=DataField(name))
def asDict(self):
"""
Represent dataform as simple dictionary mapping of datafield names to
their values
"""
ret = {}
for field in self.getTags('field'):
name = field.getAttr('var')
typ = field.getType()
if typ and typ.endswith('-multi'):
val = []
for i in field.getTags('value'):
val.append(i.getData())
else:
val = field.getTagData('value')
ret[name] = val
if self.getTag('instructions'):
ret['instructions'] = self.getInstructions()
return ret
def __getitem__(self, name):
"""
Simple dictionary interface for getting datafields values by their names
"""
item = self.getField(name)
if item:
return item.getValue()
raise IndexError('No such field')
def __setitem__(self, name, val):
"""
Simple dictionary interface for setting datafields values by their names
"""
return self.setField(name).setValue(val)
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/resolver.py 0000664 0000000 0000000 00000010161 14130341156 0021356 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from gi.repository import Gio
from gi.repository import GLib
log = logging.getLogger('nbxmpp.resolver')
class DNSResolveRequest:
def __init__(self, cache, domain, callback):
self._domain = domain
self._result = self._lookup_cache(cache)
self._callback = callback
@property
def result(self):
return self._result
@result.setter
def result(self, value):
self._result = value
@property
def is_cached(self):
return self.result is not None
def _lookup_cache(self, cache):
cached_request = cache.get(self)
if cached_request is None:
return None
return cached_request.result
def finalize(self):
GLib.idle_add(self._callback, self.result)
self._callback = None
def __hash__(self):
raise NotImplementedError
def __eq__(self, other):
return hash(other) == hash(self)
class AlternativeMethods(DNSResolveRequest):
def __init__(self, *args, **kwargs):
DNSResolveRequest.__init__(self, *args, **kwargs)
@property
def hostname(self):
return '_xmppconnect.%s' % self._domain
def __hash__(self):
return hash(self.hostname)
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args,
**kwargs)
return cls._instances[cls]
class GioResolver(metaclass=Singleton):
def __init__(self):
self._cache = {}
def _cache_request(self, request):
self._cache[request] = request
def resolve_alternatives(self, domain, callback):
request = AlternativeMethods(self._cache, domain, callback)
if request.is_cached:
request.finalize()
return
Gio.Resolver.get_default().lookup_records_async(
request.hostname,
Gio.ResolverRecordType.TXT,
None,
self._on_alternatives_result,
request)
def _on_alternatives_result(self, resolver, result, request):
try:
results = resolver.lookup_records_finish(result)
except GLib.Error as error:
log.info(error)
request.finalize()
return
try:
websocket_uri = self._parse_alternative_methods(results)
except Exception:
log.exception('Failed to parse alternative '
'connection methods: %s', results)
request.finalize()
return
request.result = websocket_uri
self._cache_request(request)
request.finalize()
@staticmethod
def _parse_alternative_methods(variant_results):
result_list = [res[0][0] for res in variant_results]
for result in result_list:
if result.startswith('_xmpp-client-websocket'):
return result.split('=')[1]
return None
if __name__ == '__main__':
import sys
try:
domain_ = sys.argv[1]
except Exception:
print('Provide domain name as argument')
sys.exit()
# Execute:
# > python3 -m nbxmpp.resolver domain
def on_result(result):
print('Result: ', result)
mainloop.quit()
GioResolver().resolve_alternatives(domain_, on_result)
mainloop = GLib.MainLoop()
mainloop.run()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/simplexml.py 0000664 0000000 0000000 00000070354 14130341156 0021541 0 ustar 00root root 0000000 0000000 ## simplexml.py based on Mattew Allum's xmlstream.py
##
## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
"""
Simplexml module provides xmpppy library with all needed tools to handle XML
nodes and XML streams. I'm personally using it in many other separate
projects. It is designed to be as standalone as possible
"""
from __future__ import annotations
from typing import Dict
from typing import List
from typing import Optional
from typing import Union
from typing import Iterator
from typing import Callable
from typing import Any
import logging
import xml.parsers.expat
from xml.parsers.expat import ExpatError
from copy import deepcopy
from nbxmpp.const import NOT_ALLOWED_XML_CHARS
Attrs = Dict[str, str]
log = logging.getLogger('nbxmpp.simplexml')
def XMLescape(text: str) -> str:
"""
Return escaped text
"""
for key, value in NOT_ALLOWED_XML_CHARS.items():
text = text.replace(key, value)
return text
class Node:
"""
Node class describes syntax of separate XML Node. It have a constructor that
permits node creation from set of "namespace name", attributes and payload
of text strings and other nodes. It does not natively support building node
from text string and uses NodeBuilder class for that purpose. After
creation node can be mangled in many ways so it can be completely changed.
Also node can be serialised into string in one of two modes: default (where
the textual representation of node describes it exactly) and "fancy" - with
whitespace added to make indentation and thus make result more readable by
human.
Node class have attribute FORCE_NODE_RECREATION that is defaults to False
thus enabling fast node replication from the some other node. The drawback
of the fast way is that new node shares some info with the "original" node
that is changing the one node may influence the other. Though it is rarely
needed (in xmpppy it is never needed at all since I'm usually never using
original node after replication (and using replication only to move upwards
on the classes tree).
"""
name: str
namespace: str
attrs: Attrs
data: List[str]
kids: List[Union[Node, str]]
parent: Optional[Node]
nsd: Dict[str, str]
nsp_cache: Dict[Any, Any]
FORCE_NODE_RECREATION = False
def __init__(
self,
tag: Optional[str] = None,
attrs: Optional[Attrs] = None,
payload: Optional[Union[Node, str, List[Union[Node, str]]]] = None,
parent: Optional[Node] = None,
nsp: Optional[Dict[Any, Any]] = None,
node_built: bool = False,
node: Optional[Union[Node, Any]] = None) -> None:
"""
Takes "tag" argument as the name of node (prepended by namespace, if
needed and separated from it by a space), attrs dictionary as the set of
arguments, payload list as the set of textual strings and child nodes
that this node carries within itself and "parent" argument that is
another node that this one will be the child of. Also the __init__ can
be provided with "node" argument that is either a text string containing
exactly one node or another Node instance to begin with. If both "node"
and other arguments is provided then the node initially created as
replica of "node" provided and then modified to be compliant with other
arguments.
"""
if node:
if self.FORCE_NODE_RECREATION and isinstance(node, Node):
node = str(node)
if not isinstance(node, Node):
node = NodeBuilder(node, self)
node_built = True
else:
self.name = node.name
self.namespace = node.namespace
self.attrs = {}
self.data = []
self.kids = []
self.parent = node.parent
self.nsd = {}
for key in node.attrs.keys():
self.attrs[key] = node.attrs[key]
for data in node.data:
self.data.append(data)
for kid in node.kids:
self.kids.append(kid)
for key, value in node.nsd.items():
self.nsd[key] = value
else:
self.name = 'tag'
self.namespace = ''
self.attrs = {}
self.data = []
self.kids = []
self.parent = None
self.nsd = {}
if parent:
self.parent = parent
self.nsp_cache = {}
if nsp:
for key, value in nsp.items():
self.nsp_cache[key] = value
if attrs is not None:
for attr, val in attrs.items():
if attr == 'xmlns':
self.nsd[''] = val
elif attr.startswith('xmlns:'):
self.nsd[attr[6:]] = val
self.attrs[attr] = attrs[attr]
if tag:
if node_built:
pfx, self.name = (['']+tag.split(':'))[-2:]
self.namespace = self.lookup_nsp(pfx)
else:
if ' ' in tag:
self.namespace, self.name = tag.split()
else:
self.name = tag
if payload is not None:
if not isinstance(payload, list):
payload = [payload]
for i in payload:
if isinstance(i, Node):
self.addChild(node=i)
else:
self.data.append(str(i))
def lookup_nsp(self, pfx: str = '') -> str:
ns = self.nsd.get(pfx, None)
if ns is None:
ns = self.nsp_cache.get(pfx, None)
if ns is None:
if self.parent:
ns = self.parent.lookup_nsp(pfx)
self.nsp_cache[pfx] = ns
else:
return 'http://www.gajim.org/xmlns/undeclared'
return ns
def __str__(self, fancy: int = 0) -> str:
"""
Method used to dump node into textual representation. If "fancy"
argument is set to True produces indented output for readability
"""
s = (fancy-1) * 2 * ' ' + "<" + self.name
if self.namespace:
if not self.parent or self.parent.namespace!=self.namespace:
if 'xmlns' not in self.attrs:
s += ' xmlns="%s"' % self.namespace
for key in self.attrs.keys():
val = str(self.attrs[key])
s += ' %s="%s"' % (key, XMLescape(val))
s += ">"
cnt = 0
if self.kids:
if fancy:
s += "\n"
for a in self.kids:
if not fancy and (len(self.data)-1) >= cnt:
s += XMLescape(self.data[cnt])
elif (len(self.data)-1) >= cnt:
s += XMLescape(self.data[cnt].strip())
if isinstance(a, str):
s += a.__str__()
else:
s += a.__str__(fancy and fancy+1)
cnt += 1
if not fancy and (len(self.data)-1) >= cnt:
s += XMLescape(self.data[cnt])
elif (len(self.data)-1) >= cnt:
s += XMLescape(self.data[cnt].strip())
if not self.kids and s.endswith('>'):
s = s[:-1] + ' />'
if fancy:
s += "\n"
else:
if fancy and not self.data:
s += (fancy-1) * 2 * ' '
s += "" + self.name + ">"
if fancy:
s += "\n"
return s
def addChild(self,
name: Optional[str] = None,
attrs: Optional[Attrs] = None,
payload: Optional[List[Any]] = None,
namespace: Optional[str] = None,
node: Optional[Node] = None) -> Node:
"""
If "node" argument is provided, adds it as child node. Else creates new
node from the other arguments' values and adds it as well
"""
if payload is None:
payload = []
if attrs is None:
attrs = {}
elif 'xmlns' in attrs:
raise AttributeError("Use namespace=x instead of attrs={'xmlns':x}")
if node:
newnode=node
node.parent = self
else: newnode=Node(tag=name, parent=self, attrs=attrs, payload=payload)
if namespace:
newnode.setNamespace(namespace)
self.kids.append(newnode)
return newnode
def addData(self, data: Any) -> None:
"""
Add some CDATA to node
"""
self.data.append(str(data))
def clearData(self) -> None:
"""
Remove all CDATA from the node
"""
self.data = []
def delAttr(self, key: str) -> None:
"""
Delete an attribute "key"
"""
del self.attrs[key]
def delChild(self,
node: Union[Node, str],
attrs: Optional[Attrs] = None) -> Optional[Node]:
"""
Delete the "node" from the node's childs list, if "node" is an instance.
Else delete the first node that have specified name and (optionally)
attributes
"""
if not isinstance(node, Node):
node = self.getTag(node, attrs)
assert isinstance(node, Node)
self.kids.remove(node)
return node
def getAttrs(self, copy: bool = False) -> Attrs:
"""
Return all node's attributes as dictionary
"""
if copy:
return deepcopy(self.attrs)
return self.attrs
def getAttr(self, key: str) -> Optional[str]:
"""
Return value of specified attribute
"""
return self.attrs.get(key)
def getChildren(self) -> List[Union[Node, str]]:
"""
Return all node's child nodes as list
"""
return self.kids
def getData(self) -> str:
"""
Return all node CDATA as string (concatenated)
"""
return ''.join(self.data)
def getName(self) -> str:
"""
Return the name of node
"""
return self.name
def getNamespace(self) -> str:
"""
Return the namespace of node
"""
return self.namespace
def getParent(self) -> Optional[Node]:
"""
Returns the parent of node (if present)
"""
return self.parent
def getPayload(self) -> List[Union[Node, str]]:
"""
Return the payload of node i.e. list of child nodes and CDATA entries.
F.e. for "text1 text2" will be returned
list: ['text1', , , ' text2']
"""
ret: List[Union[Node, str]] = []
for i in range(len(self.kids)+len(self.data)+1):
try:
if self.data[i]:
ret.append(self.data[i])
except IndexError:
pass
try:
ret.append(self.kids[i])
except IndexError:
pass
return ret
def getTag(self,
name: str,
attrs: Optional[Attrs] = None,
namespace: Optional[str] = None) -> Optional[Node]:
"""
Filter all child nodes using specified arguments as filter. Return the
first found or None if not found
"""
tag = self.getTags(name, attrs, namespace, one=True)
assert not isinstance(tag, list)
return tag
def getTagAttr(self,
tag: str,
attr: str,
namespace: Optional[str] = None) -> Optional[str]:
"""
Return attribute value of the child with specified name (or None if no
such attribute)
"""
node = self.getTag(tag, namespace=namespace)
if node is None:
return None
return node.getAttr(attr)
def getTagData(self, tag: str) -> Optional[str]:
"""
Return cocatenated CDATA of the child with specified name
"""
node = self.getTag(tag)
if node is None:
return None
return node.getData()
def getTags(self,
name: str,
attrs: Optional[Attrs] = None,
namespace: Optional[str] = None,
one: bool = False) -> Union[List[Node], Node, None]:
"""
Filter all child nodes using specified arguments as filter. Returns the
list of nodes found
"""
nodes = []
for node in self.kids:
if namespace and namespace != node.getNamespace():
continue
if node.getName() == name:
if attrs is None:
attrs = {}
for key in attrs.keys():
if key not in node.attrs or node.attrs[key] != attrs[key]:
break
else:
nodes.append(node)
if one and nodes:
return nodes[0]
if not one:
return nodes
return None
def iterTags(self,
name: str,
attrs: Optional[Attrs] = None,
namespace: Optional[str] = None) -> Iterator[Node]:
"""
Iterate over all children using specified arguments as filter
"""
for node in self.kids:
if namespace is not None and namespace != node.getNamespace():
continue
if node.getName() == name:
if attrs is None:
attrs = {}
for key in attrs.keys():
if key not in node.attrs or \
node.attrs[key]!=attrs[key]:
break
else:
yield node
def setAttr(self, key: str, val: str) -> None:
"""
Set attribute "key" with the value "val"
"""
self.attrs[key] = val
def setData(self, data: Any) -> None:
"""
Set node's CDATA to provided string. Resets all previous CDATA!
"""
self.data = [str(data)]
def setName(self, val: str) -> None:
"""
Change the node name
"""
self.name = val
def setNamespace(self, namespace: str) -> None:
"""
Changes the node namespace
"""
self.namespace = namespace
def setParent(self, node: Node) -> None:
"""
Set node's parent to "node". WARNING: do not checks if the parent
already present and not removes the node from the list of childs of
previous parent
"""
self.parent = node
def setPayload(self,
payload: Union[List[Union[Node, str]], Node, str],
add: bool = False) -> None:
"""
Set node payload according to the list specified. WARNING: completely
replaces all node's previous content. If you wish just to add child or
CDATA - use addData or addChild methods
"""
if not isinstance(payload, list):
payload = [payload]
if add:
self.kids += payload
else:
self.kids = payload
def setTag(self,
name: str,
attrs: Optional[Attrs] = None,
namespace: Optional[str] = None) -> Node:
"""
Same as getTag but if the node with specified namespace/attributes not
found, creates such node and returns it
"""
node = self.getTags(name, attrs, namespace=namespace, one=True)
if node:
return node
return self.addChild(name, attrs, namespace=namespace)
def setTagAttr(self,
tag: str,
attr: str,
val: str,
namespace: Optional[str] = None) -> None:
"""
Create new node (if not already present) with name "tag" and set it's
attribute "attr" to value "val"
"""
try:
self.getTag(tag, namespace=namespace).attrs[attr] = val
except Exception:
self.addChild(tag, namespace=namespace, attrs={attr: val})
def setTagData(self,
tag: str,
val: str,
attrs: Optional[Attrs] = None) -> None:
"""
Creates new node (if not already present) with name "tag" and
(optionally) attributes "attrs" and sets it's CDATA to string "val"
"""
try:
self.getTag(tag, attrs).setData(str(val))
except Exception:
self.addChild(tag, attrs, payload = [str(val)])
def getXmlLang(self) -> Optional[str]:
lang = self.attrs.get('xml:lang')
if lang is not None:
return lang
if self.parent is not None:
return self.parent.getXmlLang()
return None
def has_attr(self, key: str) -> bool:
"""
Check if node have attribute "key"
"""
return key in self.attrs
def __getitem__(self, item: str) -> Optional[str]:
"""
Return node's attribute "item" value
"""
return self.getAttr(item)
def __setitem__(self, item: str, val: str) -> None:
"""
Set node's attribute "item" value
"""
self.setAttr(item, val)
def __delitem__(self, item: str) -> None:
"""
Delete node's attribute "item"
"""
self.delAttr(item)
def __contains__(self, item: str) -> bool:
"""
Check if node has attribute "item"
"""
return self.has_attr(item)
def __getattr__(self, attr: str) -> Union['T', 'NT']:
"""
Reduce memory usage caused by T/NT classes - use memory only when needed
"""
if attr == 'T':
self.T = T(self)
return self.T
if attr == 'NT':
self.NT = NT(self)
return self.NT
raise AttributeError
class T:
"""
Auxiliary class used to quick access to node's child nodes
"""
def __init__(self, node):
self.__dict__['node'] = node
def __getattr__(self, attr):
return self.node.setTag(attr)
def __setattr__(self, attr, val):
if isinstance(val, Node):
Node.__init__(self.node.setTag(attr), node=val)
return None
return self.node.setTagData(attr, val)
def __delattr__(self, attr):
return self.node.delChild(attr)
class NT(T):
"""
Auxiliary class used to quick create node's child nodes
"""
def __getattr__(self, attr):
return self.node.addChild(attr)
def __setattr__(self, attr, val):
if isinstance(val, Node):
self.node.addChild(attr, node=val)
return None
return self.node.addChild(attr, payload=[val])
class NodeBuilder:
"""
Builds a Node class minidom from data parsed to it. This class used for two
purposes:
1. Creation an XML Node from a textual representation. F.e. reading a
config file. See an XML2Node method.
2. Handling an incoming XML stream. This is done by mangling the
__dispatch_depth parameter and redefining the dispatch method.
You do not need to use this class directly if you do not designing your own
XML handler
"""
_parser: Any
Parse: Callable[[str, bool], None]
__depth: int
__last_depth: int
__max_depth: int
_dispatch_depth: int
_document_attrs: Optional[Attrs]
_document_nsp: Optional[Dict[str, str]]
_mini_dom: Optional[Node]
last_is_data: bool
_ptr: Optional[Node]
data_buffer: Optional[List[str]]
streamError: str
_is_stream: bool
def __init__(self,
data: Optional[str] = None,
initial_node: Optional[Node] = None,
dispatch_depth: int = 1,
finished: bool = True) -> None:
"""
Take two optional parameters: "data" and "initial_node"
By default class initialised with empty Node class instance. Though, if
"initial_node" is provided it used as "starting point". You can think
about it as of "node upgrade". "data" (if provided) feeded to parser
immidiatedly after instance init.
"""
self._parser = xml.parsers.expat.ParserCreate()
self._parser.UseForeignDTD(False)
self._parser.StartElementHandler = self.starttag
self._parser.EndElementHandler = self.endtag
self._parser.StartNamespaceDeclHandler = self.handle_namespace_start
self._parser.CharacterDataHandler = self.handle_cdata
self._parser.StartDoctypeDeclHandler = self.handle_invalid_xmpp_element
self._parser.EntityDeclHandler = self.handle_invalid_xmpp_element
self._parser.CommentHandler = self.handle_invalid_xmpp_element
self._parser.ExternalEntityRefHandler = self.handle_invalid_xmpp_element
self._parser.AttlistDeclHandler = self.handle_invalid_xmpp_element
self._parser.ProcessingInstructionHandler = \
self.handle_invalid_xmpp_element
self._parser.buffer_text = True
self.Parse = self._parser.Parse
self.__depth = 0
self.__last_depth = 0
self.__max_depth = 0
self._dispatch_depth = dispatch_depth
self._document_attrs = None
self._document_nsp = None
self._mini_dom = initial_node
self.last_is_data = True
self._ptr = None
self.data_buffer = None
self.streamError = ''
self._is_stream = not finished
if data:
self._parser.Parse(data, finished)
def check_data_buffer(self) -> None:
if self.data_buffer:
self._ptr.data.append(''.join(self.data_buffer))
del self.data_buffer[:]
self.data_buffer = None
def destroy(self) -> None:
"""
Method used to allow class instance to be garbage-collected
"""
self.check_data_buffer()
self._parser.StartElementHandler = None
self._parser.EndElementHandler = None
self._parser.CharacterDataHandler = None
self._parser.StartNamespaceDeclHandler = None
def starttag(self, tag: str, attrs: Attrs) -> None:
"""
XML Parser callback. Used internally
"""
self.check_data_buffer()
self._inc_depth()
log.debug("STARTTAG.. DEPTH -> %i , tag -> %s, attrs -> %s",
self.__depth, tag, attrs)
if self.__depth == self._dispatch_depth:
if not self._mini_dom:
self._mini_dom = Node(tag=tag,
attrs=attrs,
nsp=self._document_nsp,
node_built=True)
else:
Node.__init__(self._mini_dom,
tag=tag,
attrs=attrs,
nsp=self._document_nsp,
node_built=True)
self._ptr = self._mini_dom
elif self.__depth > self._dispatch_depth:
self._ptr.kids.append(Node(tag=tag,
parent=self._ptr,
attrs=attrs,
node_built=True))
self._ptr = self._ptr.kids[-1]
if self.__depth == 1:
self._document_attrs = {}
self._document_nsp = {}
nsp, name = (['']+tag.split(':'))[-2:]
for attr, val in attrs.items():
if attr == 'xmlns':
self._document_nsp[''] = val
elif attr.startswith('xmlns:'):
self._document_nsp[attr[6:]] = val
else:
self._document_attrs[attr] = val
ns = self._document_nsp.get(
nsp, 'http://www.gajim.org/xmlns/undeclared-root')
try:
header = Node(tag=tag,
attrs=attrs,
nsp=self._document_nsp, node_built=True)
self.dispatch(header)
self._check_stream_start(ns, name)
except ValueError as error:
self._document_attrs = None
raise ValueError(str(error))
if not self.last_is_data and self._ptr.parent:
self._ptr.parent.data.append('')
self.last_is_data = False
def _check_stream_start(self, ns: str, tag: str) -> None:
if self._is_stream:
if ns != 'http://etherx.jabber.org/streams' or tag != 'stream':
raise ValueError('Incorrect stream start: (%s,%s). Terminating.'
% (tag, ns))
else:
self.stream_header_received()
def endtag(self, tag: str) -> None:
"""
XML Parser callback. Used internally
"""
log.debug("DEPTH -> %i , tag -> %s", self.__depth, tag)
self.check_data_buffer()
if self.__depth == self._dispatch_depth:
if self._mini_dom.getName() == 'error':
children = self._mini_dom.getChildren()
if children:
self.streamError = children[0].getName()
else:
self.streamError = self._mini_dom.getData()
self.dispatch(self._mini_dom)
elif self.__depth > self._dispatch_depth:
self._ptr = self._ptr.parent
else:
log.debug("Got higher than dispatch level. Stream terminated?")
self._dec_depth()
self.last_is_data = False
if self.__depth == 0:
self.stream_footer_received()
def handle_cdata(self, data: str) -> None:
if self.last_is_data:
if self.data_buffer:
self.data_buffer.append(data)
elif self._ptr:
self.data_buffer = [data]
self.last_is_data = True
@staticmethod
def handle_invalid_xmpp_element(*args: Any) -> None:
raise ExpatError('Found invalid xmpp stream element: %s' % str(args))
def handle_namespace_start(self, _prefix: str, _uri: str) -> None:
"""
XML Parser callback. Used internally
"""
self.check_data_buffer()
def getDom(self) -> Optional[Node]:
"""
Return just built Node
"""
self.check_data_buffer()
return self._mini_dom
def dispatch(self, stanza: Any) -> None:
"""
Get called when the NodeBuilder reaches some level of depth on it's way
up with the built node as argument. Can be redefined to convert incoming
XML stanzas to program events
"""
def stream_header_received(self) -> None:
"""
Method called when stream just opened
"""
self.check_data_buffer()
def stream_footer_received(self) -> None:
"""
Method called when stream just closed
"""
self.check_data_buffer()
def has_received_endtag(self, level: int = 0) -> bool:
"""
Return True if at least one end tag was seen (at level)
"""
return self.__depth <= level < self.__max_depth
def _inc_depth(self) -> None:
self.__last_depth = self.__depth
self.__depth += 1
self.__max_depth = max(self.__depth, self.__max_depth)
def _dec_depth(self) -> None:
self.__last_depth = self.__depth
self.__depth -= 1
def XML2Node(xml_str: str) -> Optional[Node]:
"""
Convert supplied textual string into XML node. Handy f.e. for reading
configuration file. Raises xml.parser.expat.parsererror if provided string
is not well-formed XML
"""
return NodeBuilder(xml_str).getDom()
def BadXML2Node(xml_str: str) -> Optional[Node]:
"""
Convert supplied textual string into XML node. Survives if xml data is
cutted half way round. I.e. "some text some more text". Will raise
xml.parser.expat.parsererror on misplaced tags though. F.e. "some text
some more text" will not work
"""
return NodeBuilder(xml_str).getDom()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/smacks.py 0000664 0000000 0000000 00000027024 14130341156 0021004 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import time
import logging
from nbxmpp.namespaces import Namespace
from nbxmpp.simplexml import Node
from nbxmpp.const import StreamState
from nbxmpp.util import LogAdapter
from nbxmpp.structs import StanzaHandler
from nbxmpp.protocol import Error
log = logging.getLogger('nbxmpp.smacks')
class Smacks:
"""
This is Smacks is the Stream Management class. It takes care of requesting
and sending acks. Also, it keeps track of the unhandled outgoing stanzas.
The dispatcher has to be able to access this class to increment the
number of handled stanzas
"""
def __init__(self, client):
self._client = client
self._out_h = 0 # Outgoing stanzas handled
self._in_h = 0 # Incoming stanzas handled
self._acked_h = 0 # Last acked stanza
self._uqueue = [] # Unhandled stanzas queue
self._old_uqueue = [] # Unhandled stanzas queue of the last session
# Max number of stanzas in queue before making a request
self.max_queue = 0
self._sm_supported = False
self.enabled = False # If SM is enabled
self._enable_sent = False # If we sent 'enable'
self.resumed = False # If the session was resumed
self.resume_in_progress = False
self.resume_supported = False # Does the session support resume
self._session_id = None
self._location = None
self._log = LogAdapter(log, {'context': client.log_context})
self.register_handlers()
@property
def sm_supported(self):
return self._sm_supported
@sm_supported.setter
def sm_supported(self, value):
self._log.info('Server supports detected: %s', value)
self._sm_supported = value
def delegate(self, stanza):
if stanza.getNamespace() != Namespace.STREAM_MGMT:
return
if stanza.getName() == 'resumed':
self._on_resumed(stanza)
elif stanza.getName() == 'failed':
self._on_failed(None, stanza, None)
def register_handlers(self):
handlers = [
StanzaHandler(name='enabled',
callback=self._on_enabled,
xmlns=Namespace.STREAM_MGMT),
StanzaHandler(name='failed',
callback=self._on_failed,
xmlns=Namespace.STREAM_MGMT),
StanzaHandler(name='r',
callback=self._send_ack,
xmlns=Namespace.STREAM_MGMT),
StanzaHandler(name='a',
callback=self._on_ack,
xmlns=Namespace.STREAM_MGMT)
]
for handler in handlers:
self._client.register_handler(handler)
def send_enable(self):
if not self.sm_supported:
return
if self._client.sm_disabled:
return
enable = Node(Namespace.STREAM_MGMT + ' enable', attrs={'resume': 'true'})
self._client.send_nonza(enable, now=False)
self._log.debug('Send enable')
self._enable_sent = True
def _on_enabled(self, _con, stanza, _properties):
if self.enabled:
self._log.error('Received "enabled", but SM is already enabled')
return
resume = stanza.getAttr('resume')
if resume in ('true', 'True', '1'):
self.resume_supported = True
self._session_id = stanza.getAttr('id')
self._location = stanza.getAttr('location')
self.enabled = True
self._log.info(
'Received enabled, location: %s, resume supported: %s, '
'session-id: %s', self._location, resume, self._session_id)
def count_incoming(self, name):
if not self.enabled:
# Dont count while we didnt receive 'enabled'
return
if name in ('a', 'r', 'resumed', 'enabled'):
return
self._log.debug('IN, %s', name)
self._in_h += 1
def save_in_queue(self, stanza):
if not self._enable_sent and not self.resumed:
# We did not yet sent 'enable' so the server
# will not count our stanzas
return
# Make a full copy so we dont run into problems when
# the stanza is modified after sending for some reason
# TODO: Make also copies of Protocol.Error objects
if not isinstance(stanza, Error):
stanza = type(stanza)(node=str(stanza))
self._add_delay(stanza)
self._uqueue.append(stanza)
self._log.debug('OUT, %s', stanza.getName())
self._out_h += 1
if len(self._uqueue) > self.max_queue:
self._request_ack()
# Send an ack after 100 unacked messages
if (self._in_h - self._acked_h) > 100:
self._send_ack()
def _add_delay(self, stanza):
if stanza.getName() != 'message':
return
if stanza.getType() not in ('chat', 'groupchat'):
return
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
attrs = {'stamp': timestamp}
if stanza.getType() != 'groupchat':
# Dont leak our JID to Groupchats
attrs['from'] = str(self._client.get_bound_jid())
stanza.addChild('delay', namespace=Namespace.DELAY2, attrs=attrs)
def _resend_queue(self):
"""
Resends unsent stanzas when a new session is established.
This way there won't be any lost outgoing messages even on failed
smacks resumes (but message duplicates are possible).
"""
if not self._old_uqueue:
return
self._log.info('Resend %s stanzas', len(self._old_uqueue))
for stanza in self._old_uqueue:
# Use dispatcher so we increment the counter
self._client.send_stanza(stanza)
self._old_uqueue = []
def resume_request(self):
if self._session_id is None:
self._log.error('Attempted to resume without a valid session id')
return
# Save old messages in an extra "queue" to avoid race conditions
# and to make it possible to replay stanzas even when resuming fails
# Add messages here (instead of overwriting) so that repeated
# connection errors don't delete unacked stanzas
# (uqueue should be empty in this case anyways)
self._old_uqueue += self._uqueue
self._uqueue = []
resume = Node(Namespace.STREAM_MGMT + ' resume',
attrs={'h': self._in_h, 'previd': self._session_id})
self._acked_h = self._in_h
self.resume_in_progress = True
self._client.send_nonza(resume, now=False)
def _on_resumed(self, stanza):
"""
Checks if the number of stanzas sent are the same as the
number of stanzas received by the server. Resends stanzas not received
by the server in the last session.
"""
self._log.info('Session resumption succeeded, session-id: %s',
self._session_id)
self._validate_ack(stanza, self._old_uqueue)
# Set our out h to the h we received
self._out_h = int(stanza.getAttr('h'))
self.enabled = True
self.resumed = True
self.resume_in_progress = False
self._client.set_state(StreamState.RESUME_SUCCESSFUL)
self._resend_queue()
def _send_ack(self, *args):
ack = Node(Namespace.STREAM_MGMT + ' a', attrs={'h': self._in_h})
self._acked_h = self._in_h
self._log.debug('Send ack, h: %s', self._in_h)
self._client.send_nonza(ack, now=False)
def close_session(self):
# We end the connection deliberately
# Reset the state -> no resume
self._log.info('Close session')
self._reset_state()
def _request_ack(self):
request = Node(Namespace.STREAM_MGMT + ' r')
self._log.debug('Request ack')
self._client.send_nonza(request, now=False)
def _on_ack(self, _stream, stanza, _properties):
if not self.enabled:
return
self._log.debug('Ack received, h: %s', stanza.getAttr('h'))
self._validate_ack(stanza, self._uqueue)
def _validate_ack(self, stanza, queue):
"""
Checks if the number of stanzas sent are the same as the
number of stanzas received by the server. Pops stanzas that were
handled by the server from the queue.
"""
count_server = stanza.getAttr('h')
if count_server is None:
self._log.error('Server did not send h attribute')
return
count_server = int(count_server)
diff = self._out_h - count_server
queue_size = len(queue)
if diff < 0:
self._log.error(
'Mismatch detected, our h: %d, server h: %d, queue: %d',
self._out_h, count_server, queue_size)
# Don't accumulate all messages in this case
# (they would otherwise all be resent on the next reconnect)
queue = []
elif queue_size < diff:
self._log.error(
'Mismatch detected, our h: %d, server h: %d, queue: %d',
self._out_h, count_server, queue_size)
else:
self._log.debug('Validate ack, our h: %d, server h: %d, queue: %d',
self._out_h, count_server, queue_size)
self._log.debug('Removing %d stanzas from queue', queue_size - diff)
while len(queue) > diff:
queue.pop(0)
def _on_failed(self, _stream, stanza, _properties):
'''
This can be called after 'enable' and 'resume'
'''
self._log.info('Negotiation failed')
error_text = stanza.getTagData('text')
if error_text is not None:
self._log.info(error_text)
if stanza.getTag('item-not-found') is not None:
self._log.info('Session timed out, last server h: %s',
stanza.getAttr('h'))
self._validate_ack(stanza, self._old_uqueue)
else:
for tag in stanza.getChildren():
if tag.getName() != 'text':
self._log.info(tag.getName())
if self.resume_in_progress:
# Reset state before sending Bind, because otherwise stanza
# will be counted and ack will be requested.
# _reset_state() also resets resume_in_progress
self._reset_state()
self._client.set_state(StreamState.RESUME_FAILED)
self._reset_state()
def _reset_state(self):
# Reset all values to default
self._out_h = 0
self._in_h = 0
self._acked_h = 0
self._uqueue = []
self._old_uqueue = []
self.enabled = False
self._enable_sent = False
self.resumed = False
self.resume_in_progress = False
self.resume_supported = False
self._session_id = None
self._location = None
self._enable_sent = False
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/structs.py 0000664 0000000 0000000 00000067361 14130341156 0021242 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018-2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import time
import random
from collections import namedtuple
from gi.repository import Soup
from gi.repository import Gio
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Protocol
from nbxmpp.protocol import Node
from nbxmpp.const import MessageType
from nbxmpp.const import AvatarState
from nbxmpp.const import StatusCode
from nbxmpp.const import PresenceType
from nbxmpp.const import LOCATION_DATA
from nbxmpp.const import AdHocStatus
StanzaHandler = namedtuple('StanzaHandler',
'name callback typ ns xmlns priority')
StanzaHandler.__new__.__defaults__ = ('', '', None, 50)
CommonResult = namedtuple('CommonResult', 'jid')
CommonResult.__new__.__defaults__ = (None,)
InviteData = namedtuple('InviteData',
'muc from_ reason password type continued thread')
DeclineData = namedtuple('DeclineData', 'muc from_ reason')
CaptchaData = namedtuple('CaptchaData', 'form bob_data')
BobData = namedtuple('BobData', 'algo hash_ max_age data cid type')
VoiceRequest = namedtuple('VoiceRequest', 'form jid nick')
MucUserData = namedtuple('MucUserData', 'affiliation jid nick role actor reason')
MucUserData.__new__.__defaults__ = (None, None, None, None, None)
MucDestroyed = namedtuple('MucDestroyed', 'alternate reason password')
MucDestroyed.__new__.__defaults__ = (None, None, None)
MucConfigResult = namedtuple('MucConfigResult', 'jid form')
MucConfigResult.__new__.__defaults__ = (None,)
AffiliationResult = namedtuple('AffiliationResult', 'jid users')
EntityCapsData = namedtuple('EntityCapsData', 'hash node ver')
EntityCapsData.__new__.__defaults__ = (None, None, None)
HTTPAuthData = namedtuple('HTTPAuthData', 'id method url body')
HTTPAuthData.__new__.__defaults__ = (None, None, None, None)
StanzaIDData = namedtuple('StanzaIDData', 'id by')
StanzaIDData.__new__.__defaults__ = (None, None)
PubSubEventData = namedtuple('PubSubEventData', 'node id item data deleted retracted purged')
PubSubEventData.__new__.__defaults__ = (None, None, None, False, False, False)
MoodData = namedtuple('MoodData', 'mood text')
BlockingPush = namedtuple('BlockingPush', 'block unblock unblock_all')
ActivityData = namedtuple('ActivityData', 'activity subactivity text')
LocationData = namedtuple('LocationData', LOCATION_DATA)
LocationData.__new__.__defaults__ = (None,) * len(LocationData._fields)
BookmarkData = namedtuple('BookmarkData', 'jid name nick autojoin password')
BookmarkData.__new__.__defaults__ = (None, None, None, None)
PGPPublicKey = namedtuple('PGPPublicKey', 'jid key date')
PGPKeyMetadata = namedtuple('PGPKeyMetadata', 'jid fingerprint date')
OMEMOMessage = namedtuple('OMEMOMessage', 'sid iv keys payload')
AnnotationNote = namedtuple('AnnotationNote', 'jid data cdate mdate')
AnnotationNote.__new__.__defaults__ = (None, None)
EMEData = namedtuple('EMEData', 'name namespace')
MuclumbusResult = namedtuple('MuclumbusResult', 'first last max end items')
MuclumbusItem = namedtuple('MuclumbusItem', 'jid name nusers description language is_open anonymity_mode')
SoftwareVersionResult = namedtuple('SoftwareVersionResult', 'name version os')
AdHocCommandNote = namedtuple('AdHocCommandNote', 'text type')
IBBData = namedtuple('IBBData', 'block_size sid seq type data')
IBBData.__new__.__defaults__ = (None, None, None, None, None)
DiscoItems = namedtuple('DiscoItems', 'jid node items')
DiscoItem = namedtuple('DiscoItem', 'jid name node')
DiscoItem.__new__.__defaults__ = (None, None)
OOBData = namedtuple('OOBData', 'url desc')
CorrectionData = namedtuple('CorrectionData', 'id')
RegisterData = namedtuple('RegisterData', 'instructions form fields_form oob_url bob_data')
HTTPUploadData = namedtuple('HTTPUploadData', 'put_uri get_uri headers')
HTTPUploadData.__new__.__defaults__ = (None,)
RSMData = namedtuple('RSMData', 'after before last first first_index count max index')
MAMQueryData = namedtuple('MAMQueryData', 'jid rsm complete')
MAMPreferencesData = namedtuple('MAMPreferencesData', 'default always never')
class DiscoInfo(namedtuple('DiscoInfo', 'stanza identities features dataforms timestamp')):
__slots__ = []
def __new__(cls, stanza, identities, features, dataforms, timestamp=None):
return super(DiscoInfo, cls).__new__(cls, stanza, identities,
features, dataforms, timestamp)
def get_caps_hash(self):
try:
return self.node.split('#')[1]
except Exception:
return None
def has_field(self, form_type, var):
for dataform in self.dataforms:
try:
if dataform['FORM_TYPE'].value != form_type:
continue
if var in dataform.vars:
return True
except Exception:
continue
return False
def get_field_value(self, form_type, var):
for dataform in self.dataforms:
try:
if dataform['FORM_TYPE'].value != form_type:
continue
if dataform[var].type_ == 'jid-multi':
return dataform[var].values or None
return dataform[var].value or None
except Exception:
continue
def supports(self, feature):
return feature in self.features
def serialize(self):
if self.stanza is None:
raise ValueError('Unable to serialize DiscoInfo, no stanza found')
return str(self.stanza)
@property
def node(self):
try:
query = self.stanza.getQuery()
except Exception:
return None
if query is not None:
return query.getAttr('node')
return None
@property
def jid(self):
try:
return self.stanza.getFrom()
except Exception:
return None
@property
def mam_namespace(self):
if Namespace.MAM_2 in self.features:
return Namespace.MAM_2
if Namespace.MAM_1 in self.features:
return Namespace.MAM_1
return None
@property
def has_mam_2(self):
return Namespace.MAM_2 in self.features
@property
def has_mam_1(self):
return Namespace.MAM_1 in self.features
@property
def has_mam(self):
return self.has_mam_1 or self.has_mam_2
@property
def has_httpupload(self):
return Namespace.HTTPUPLOAD_0 in self.features
@property
def is_muc(self):
for identity in self.identities:
if identity.category == 'conference':
if Namespace.MUC in self.features:
return True
return False
@property
def muc_name(self):
if self.muc_room_name:
return self.muc_room_name
if self.muc_identity_name:
return self.muc_identity_name
if self.jid is not None:
return self.jid.localpart
return None
@property
def muc_identity_name(self):
for identity in self.identities:
if identity.category == 'conference':
return identity.name
return None
@property
def muc_room_name(self):
return self.get_field_value(Namespace.MUC_INFO, 'muc#roomconfig_roomname')
@property
def muc_description(self):
return self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_description')
@property
def muc_log_uri(self):
return self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_logs')
@property
def muc_users(self):
return self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_occupants')
@property
def muc_contacts(self):
return self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_contactjid')
@property
def muc_subject(self):
return self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_subject')
@property
def muc_subjectmod(self):
# muc#roominfo_changesubject stems from a wrong example in the MUC XEP
# Ejabberd and Prosody use this value
return (self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_subjectmod') or
self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_changesubject'))
@property
def muc_lang(self):
return self.get_field_value(Namespace.MUC_INFO, 'muc#roominfo_lang')
@property
def muc_is_persistent(self):
return 'muc_persistent' in self.features
@property
def muc_is_moderated(self):
return 'muc_moderated' in self.features
@property
def muc_is_open(self):
return 'muc_open' in self.features
@property
def muc_is_members_only(self):
return 'muc_membersonly' in self.features
@property
def muc_is_hidden(self):
return 'muc_hidden' in self.features
@property
def muc_is_nonanonymous(self):
return 'muc_nonanonymous' in self.features
@property
def muc_is_passwordprotected(self):
return 'muc_passwordprotected' in self.features
@property
def muc_is_public(self):
return 'muc_public' in self.features
@property
def muc_is_semianonymous(self):
return 'muc_semianonymous' in self.features
@property
def muc_is_temporary(self):
return 'muc_temporary' in self.features
@property
def muc_is_unmoderated(self):
return 'muc_unmoderated' in self.features
@property
def muc_is_unsecured(self):
return 'muc_unsecured' in self.features
@property
def is_gateway(self):
for identity in self.identities:
if identity.category == 'gateway':
return True
return False
@property
def gateway_name(self):
for identity in self.identities:
if identity.category == 'gateway':
return identity.name
return None
@property
def gateway_type(self):
for identity in self.identities:
if identity.category == 'gateway':
return identity.type
return None
def has_category(self, category):
for identity in self.identities:
if identity.category == category:
return True
return False
@property
def httpupload_max_file_size(self):
size = self.get_field_value(Namespace.HTTPUPLOAD_0, 'max-file-size')
try:
return float(size)
except Exception:
return None
class DiscoIdentity(namedtuple('DiscoIdentity', 'category type name lang')):
__slots__ = []
def __new__(cls, category, type, name=None, lang=None):
return super(DiscoIdentity, cls).__new__(cls, category, type, name, lang)
def get_node(self):
identity = Node('identity',
attrs={'category': self.category,
'type': self.type})
if self.name is not None:
identity.setAttr('name', self.name)
if self.lang is not None:
identity.setAttr('xml:lang', self.lang)
return identity
def __eq__(self, other):
return str(self) == str(other)
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return '%s/%s/%s/%s' % (self.category,
self.type,
self.lang or '',
self.name or '')
def __hash__(self):
return hash(str(self))
class AdHocCommand(namedtuple('AdHocCommand', 'jid node name sessionid status data actions default notes')):
__slots__ = []
def __new__(cls, jid, node, name, sessionid=None, status=None,
data=None, actions=None, default=None, notes=None):
return super(AdHocCommand, cls).__new__(cls, jid, node, name, sessionid,
status, data, actions, default, notes)
@property
def is_completed(self):
return self.status == AdHocStatus.COMPLETED
@property
def is_canceled(self):
return self.status == AdHocStatus.CANCELED
class ProxyData(namedtuple('ProxyData', 'type host username password')):
__slots__ = []
def get_uri(self):
if self.username is not None:
user_pass = Soup.uri_encode('%s:%s' % (self.username,
self.password))
return '%s://%s@%s' % (self.type,
user_pass,
self.host)
return '%s://%s' % (self.type, self.host)
def get_resolver(self):
return Gio.SimpleProxyResolver.new(self.get_uri(), None)
class OMEMOBundle(namedtuple('OMEMOBundle', 'spk spk_signature ik otpks')):
def pick_prekey(self):
return random.SystemRandom().choice(self.otpks)
class ChatMarker(namedtuple('ChatMarker', 'type id')):
@property
def is_received(self):
return self.type == 'received'
@property
def is_displayed(self):
return self.type == 'displayed'
@property
def is_acknowledged(self):
return self.type == 'acknowledged'
class CommonError:
def __init__(self, stanza):
self._stanza_name = stanza.getName()
self._error_node = stanza.getTag('error')
self.condition = stanza.getError()
self.condition_data = self._error_node.getTagData(self.condition)
self.app_condition = stanza.getAppError()
self.type = stanza.getErrorType()
self.jid = stanza.getFrom()
self.id = stanza.getID()
self._text = {}
text_elements = self._error_node.getTags('text', namespace=Namespace.STANZAS)
for element in text_elements:
lang = element.getXmlLang()
text = element.getData()
self._text[lang] = text
@classmethod
def from_string(cls, node_string):
return cls(Protocol(node=node_string))
def get_text(self, pref_lang=None):
if pref_lang is not None:
text = self._text.get(pref_lang)
if text is not None:
return text
if self._text:
text = self._text.get('en')
if text is not None:
return text
text = self._text.get(None)
if text is not None:
return text
return self._text.popitem()[1]
return ''
def set_text(self, lang, text):
self._text[lang] = text
def __str__(self):
condition = self.condition
if self.app_condition is not None:
condition = '%s (%s)' % (self.condition, self.app_condition)
text = self.get_text('en') or ''
if text:
text = ' - %s' % text
return 'Error from %s: %s%s' % (self.jid, condition, text)
def serialize(self):
return str(Protocol(name=self._stanza_name,
frm=self.jid,
xmlns=Namespace.CLIENT,
attrs={'id': self.id},
payload=self._error_node))
class HTTPUploadError(CommonError):
def __init__(self, stanza):
CommonError.__init__(self, stanza)
def get_max_file_size(self):
if not self.app_condition == 'file-too-large':
return None
node = self._error_node.getTag(self.app_condition)
try:
return float(node.getTagData('max-file-size'))
except Exception:
return None
def get_retry_date(self):
if not self.app_condition == 'retry':
return None
return self._error_node.getTagAttr('stamp')
class StanzaMalformedError(CommonError):
def __init__(self, stanza, text):
self._error_node = None
self.condition = 'stanza-malformed'
self.condition_data = None
self.app_condition = None
self.type = None
self.jid = stanza.getFrom()
self.id = stanza.getID()
self._text = {}
if text:
self._text['en'] = text
@classmethod
def from_string(cls, node_string):
raise NotImplementedError
def __str__(self):
text = self.get_text('en')
if text:
text = ': %s' % text
return 'Received malformed stanza from %s%s' % (self.jid, text)
def serialize(self):
raise NotImplementedError
class StreamError(CommonError):
def __init__(self, stanza):
self.condition = stanza.getError()
self.condition_data = self._error_node.getTagData(self.condition)
self.app_condition = stanza.getAppError()
self.type = stanza.getErrorType()
self.jid = stanza.getFrom()
self.id = stanza.getID()
self._text = {}
text_elements = self._error_node.getTags('text', namespace=Namespace.STREAMS)
for element in text_elements:
lang = element.getXmlLang()
text = element.getData()
self._text[lang] = text
@classmethod
def from_string(cls, node_string):
raise NotImplementedError
def __str__(self):
text = self.get_text('en') or ''
if text:
text = ' - %s' % text
return 'Error from %s: %s%s' % (self.jid, self.condition, text)
def serialize(self):
raise NotImplementedError
class TuneData(namedtuple('TuneData', 'artist length rating source title track uri')):
__slots__ = []
def __new__(cls, artist=None, length=None, rating=None, source=None,
title=None, track=None, uri=None):
return super(TuneData, cls).__new__(cls, artist, length, rating,
source, title, track, uri)
@property
def was_removed(self):
return (self.artist is None and
self.title is None and
self.track is None)
class MAMData(namedtuple('MAMData', 'id query_id archive namespace timestamp')):
__slots__ = []
@property
def is_ver_1(self):
return self.namespace == Namespace.MAM_1
@property
def is_ver_2(self):
return self.namespace == Namespace.MAM_2
class CarbonData(namedtuple('MAMData', 'type')):
__slots__ = []
@property
def is_sent(self):
return self.type == 'sent'
@property
def is_received(self):
return self.type == 'received'
class ReceiptData(namedtuple('ReceiptData', 'type id')):
__slots__ = []
def __new__(cls, type, id=None):
return super(ReceiptData, cls).__new__(cls, type, id)
@property
def is_request(self):
return self.type == 'request'
@property
def is_received(self):
return self.type == 'received'
class Properties:
pass
class MessageProperties:
def __init__(self, own_jid):
self._own_jid = own_jid
self.carbon = None
self.type = MessageType.NORMAL
self.id = None
self.stanza_id = None
self.from_ = None
self.to = None
self.jid = None
self.subject = None
self.body = None
self.thread = None
self.user_timestamp = None
self.timestamp = time.time()
self.has_server_delay = False
self.error = None
self.eme = None
self.http_auth = None
self.nickname = None
self.from_muc = False
self.muc_jid = None
self.muc_nickname = None
self.muc_status_codes = None
self.muc_private_message = False
self.muc_invite = None
self.muc_decline = None
self.muc_user = None
self.muc_ofrom = None
self.captcha = None
self.voice_request = None
self.self_message = False
self.mam = None
self.pubsub = False
self.pubsub_event = None
self.openpgp = None
self.omemo = None
self.encrypted = None
self.pgp_legacy = None
self.marker = None
self.receipt = None
self.oob = None
self.correction = None
self.attention = False
self.forms = None
self.xhtml = None
self.security_label = None
self.chatstate = None
def is_from_us(self, bare_match=True):
if self.from_ is None:
raise ValueError('from attribute missing')
if bare_match:
return self._own_jid.bare_match(self.from_)
return self._own_jid == self.from_
@property
def has_user_delay(self):
return self.user_timestamp is not None
@property
def is_encrypted(self):
return self.encrypted is not None
@property
def is_omemo(self):
return self.omemo is not None
@property
def is_openpgp(self):
return self.openpgp is not None
@property
def is_pgp_legacy(self):
return self.pgp_legacy is not None
@property
def is_pubsub(self):
return self.pubsub
@property
def is_pubsub_event(self):
return self.pubsub_event is not None
@property
def is_carbon_message(self):
return self.carbon is not None
@property
def is_sent_carbon(self):
return self.carbon is not None and self.carbon.is_sent
@property
def is_received_carbon(self):
return self.carbon is not None and self.carbon.is_received
@property
def is_mam_message(self):
return self.mam is not None
@property
def is_http_auth(self):
return self.http_auth is not None
@property
def is_muc_subject(self):
return (self.type == MessageType.GROUPCHAT and
self.body is None and
self.subject is not None)
@property
def is_muc_config_change(self):
return self.body is None and self.muc_status_codes
@property
def is_muc_pm(self):
return self.muc_private_message
@property
def is_muc_invite_or_decline(self):
return (self.muc_invite is not None or
self.muc_decline is not None)
@property
def is_captcha_challenge(self):
return self.captcha is not None
@property
def is_voice_request(self):
return self.voice_request is not None
@property
def is_self_message(self):
return self.self_message
@property
def is_marker(self):
return self.marker is not None
@property
def is_receipt(self):
return self.receipt is not None
@property
def is_oob(self):
return self.oob is not None
@property
def is_correction(self):
return self.correction is not None
@property
def has_attention(self):
return self.attention
@property
def has_forms(self):
return self.forms is not None
@property
def has_xhtml(self):
return self.xhtml is not None
@property
def has_security_label(self):
return self.security_label is not None
@property
def has_chatstate(self):
return self.chatstate is not None
class IqProperties:
def __init__(self, own_jid):
self._own_jid = own_jid
self.type = None
self.jid = None
self.id = None
self.error = None
self.query = None
self.payload = None
self.http_auth = None
self.ibb = None
self.blocking = None
@property
def is_http_auth(self):
return self.http_auth is not None
@property
def is_ibb(self):
return self.ibb is not None
@property
def is_blocking(self):
return self.blocking is not None
class PresenceProperties:
def __init__(self, own_jid):
self._own_jid = own_jid
self.type = None
self.priority = None
self.show = None
self.jid = None
self.resource = None
self.id = None
self.payload = None
self.query = None
self.nickname = None
self.self_presence = False
self.self_bare = False
self.from_muc = False
self.status = ''
self.timestamp = time.time()
self.user_timestamp = None
self.idle_timestamp = None
self.signed = None
self.error = None
self.avatar_sha = None
self.avatar_state = AvatarState.IGNORE
self.muc_jid = None
self.muc_status_codes = None
self.muc_user = None
self.muc_nickname = None
self.muc_destroyed = None
self.entity_caps = None
@property
def is_self_presence(self):
return self.self_presence
@property
def is_self_bare(self):
return self.self_bare
@property
def is_muc_destroyed(self):
return self.muc_destroyed is not None
@property
def is_muc_self_presence(self):
return (self.from_muc and
self.muc_status_codes is not None and
StatusCode.SELF in self.muc_status_codes)
@property
def is_nickname_modified(self):
return (self.from_muc and
self.muc_status_codes is not None and
StatusCode.NICKNAME_MODIFIED in self.muc_status_codes and
self.type == PresenceType.AVAILABLE)
@property
def is_nickname_changed(self):
return (self.from_muc and
self.muc_status_codes is not None and
StatusCode.NICKNAME_CHANGE in self.muc_status_codes and
self.muc_user.nick is not None and
self.type == PresenceType.UNAVAILABLE)
@property
def new_jid(self):
if not self.is_nickname_changed:
raise ValueError('This is not a nickname change')
return self.jid.new_with(resource=self.muc_user.nick)
@property
def is_kicked(self):
status_codes = {
StatusCode.REMOVED_BANNED,
StatusCode.REMOVED_KICKED,
StatusCode.REMOVED_AFFILIATION_CHANGE,
StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY,
StatusCode.REMOVED_SERVICE_SHUTDOWN,
StatusCode.REMOVED_ERROR
}
return (self.from_muc and
self.muc_status_codes is not None and
status_codes.intersection(self.muc_status_codes) and
self.type == PresenceType.UNAVAILABLE)
@property
def is_muc_shutdown(self):
return (self.from_muc and
self.muc_status_codes is not None and
StatusCode.REMOVED_SERVICE_SHUTDOWN in self.muc_status_codes)
@property
def is_new_room(self):
status_codes = {
StatusCode.CREATED,
StatusCode.SELF
}
return (self.from_muc and
self.muc_status_codes is not None and
status_codes.issubset(self.muc_status_codes))
@property
def affiliation(self):
try:
return self.muc_user.affiliation
except Exception:
return None
@property
def role(self):
try:
return self.muc_user.role
except Exception:
return None
class XHTMLData:
def __init__(self, xhtml):
self._bodys = {}
for body in xhtml.getTags('body', namespace=Namespace.XHTML):
lang = body.getXmlLang()
self._bodys[lang] = body
def get_body(self, pref_lang=None):
if pref_lang is not None:
body = self._bodys.get(pref_lang)
if body is not None:
return str(body)
body = self._bodys.get('en')
if body is not None:
return str(body)
body = self._bodys.get(None)
if body is not None:
return str(body)
return str(self._bodys.popitem()[1])
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/task.py 0000664 0000000 0000000 00000022632 14130341156 0020465 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import weakref
import inspect
import logging
from enum import IntEnum
from functools import wraps
from gi.repository import Soup
from nbxmpp.errors import is_error
from nbxmpp.errors import CancelledError
from nbxmpp.errors import TimeoutStanzaError
from nbxmpp.simplexml import Node
log = logging.getLogger('nbxmpp.task')
class _ResultSet:
pass
ResultSet = _ResultSet()
class NoType:
pass
class TaskState(IntEnum):
INIT = 0
RUNNING = 1
FINISHED = 2
CANCELLED = 3
@property
def is_init(self):
return self == TaskState.INIT
@property
def is_running(self):
return self == TaskState.RUNNING
@property
def is_finished(self):
return self == TaskState.FINISHED
@property
def is_cancelled(self):
return self == TaskState.CANCELLED
def _setup_task(task, client, callback, user_data):
client.add_task(task)
task.set_finalize_func(client.remove_task)
task.set_user_data(user_data)
if callback is not None:
task.add_done_callback(callback)
task.start()
return task
def iq_request_task(func):
@wraps(func)
def func_wrapper(self, *args, timeout=None, callback=None, user_data=None, **kwargs):
task = IqRequestTask(func(self, *args, **kwargs),
self._log,
self._client)
task.set_timeout(timeout)
return _setup_task(task, self._client, callback, user_data)
return func_wrapper
def http_request_task(func):
@wraps(func)
def func_wrapper(self, *args, callback=None, user_data=None, **kwargs):
task = HTTPRequestTask(func(self, *args, **kwargs),
self._log,
self._soup_session)
return _setup_task(task, self._client, callback, user_data)
return func_wrapper
def is_fatal_error(error):
if is_error(error):
return error.is_fatal
return isinstance(error, Exception)
class Task:
'''
Base class for wrapping a generator method.
It runs the generator depending on what the generator yields. If the
generator yields another generator a sub task is created. If it yields
a type defined in _process_types, _run_async() is called which needs to
be implemented by classes.
the implementation of _run_async() must be really async, means it should
not call _async_finished() in the same mainloop cycle. Otherwise sub tasks
may break. _async_finished() needs to call _next_step(result).
'''
_process_types = (NoType,)
def __init__(self, gen, logger=log):
self._logger = logger
self._gen = gen
self._done_callbacks = []
self._sub_task = None
self._result = None
self._error = None
self._user_data = None
self._timeout = None
self._finalize_func = None
self._finalize_context = None
self._state = TaskState.INIT
@property
def state(self):
return self._state
def add_done_callback(self, callback, weak=True):
if self._state.is_finished or self._state.is_cancelled:
raise RuntimeError('Task is finished')
if weak:
if inspect.ismethod(callback):
callback = weakref.WeakMethod(callback)
elif inspect.isfunction(callback):
callback = weakref.ref(callback)
else:
ValueError('Unknown callback object')
self._done_callbacks.append(callback)
def set_timeout(self, timeout):
self._timeout = timeout
def start(self):
if not self._state.is_init:
raise RuntimeError('Task already started')
self._state = TaskState.RUNNING
next(self._gen)
self._next_step(self)
def _run_async(self, data):
raise NotImplementedError
def _async_finished(self, *args, **kwargs):
raise NotImplementedError
def _sub_task_completed(self, task):
self._sub_task = None
if not self._state.is_running:
return
result = task.get_result()
if is_fatal_error(result):
self._error = result
self._set_finished()
else:
self._next_step(result)
def _next_step(self, result):
try:
res = self._gen.send(result)
except StopIteration:
self._set_finished()
return
except Exception as error:
self._log_if_fatal(error)
self._error = error
self._set_finished()
return
if isinstance(res, self._process_types):
self._run_async(res)
elif isinstance(res, Task):
if self._sub_task is not None:
RuntimeError('Only one sub task can be active')
self._sub_task = res
self._sub_task.add_done_callback(self._sub_task_completed,
weak=False)
else:
if res is not ResultSet:
self._result = res
self._set_finished()
def _set_finished(self):
self._state = TaskState.FINISHED
self._invoke_callbacks()
self._finalize()
def _log_if_fatal(self, error):
if is_error(error):
if error.is_fatal:
self._logger.log(error.log_level, error)
elif isinstance(error, Exception):
self._logger.exception('Fatal Exception')
def _invoke_callbacks(self):
for callback in self._done_callbacks:
if isinstance(callback, weakref.WeakMethod):
callback = callback()
if callback is None:
return
# Be conservative with catching exceptions here
# For example unittests raise Assertion errors
# which should not be catched here
try:
callback(self)
except CancelledError:
pass
def set_result(self, result):
self._result = result
return ResultSet
def get_result(self):
# if self._error is None, there was no error
# but None is a valid value for self._result
if self._error is not None:
return self._error
return self._result
def finish(self):
if self._error is not None:
raise self._error # pylint: disable=raising-bad-type
return self._result
def set_user_data(self, user_data):
self._user_data = user_data
def get_user_data(self):
return self._user_data
def set_finalize_func(self, func, context=None):
self._finalize_func = func
self._finalize_context = context
def cancel(self):
if not self._state.is_running:
return
self._state = TaskState.CANCELLED
if self._sub_task is not None:
self._sub_task.cancel()
self._error = CancelledError()
self._invoke_callbacks()
self._finalize()
def _finalize(self):
self._done_callbacks.clear()
self._sub_task = None
self._error = None
self._result = None
self._user_data = None
self._gen.close()
if self._finalize_func is not None:
self._finalize_func(self, self._finalize_context)
class IqRequestTask(Task):
'''
A Task for running IQ requests
'''
_process_types = (Node,)
def __init__(self, gen, logger, client):
super().__init__(gen, logger)
self._client = client
self._iq_id = None
def _run_async(self, stanza):
self._iq_id = self._client.send_stanza(stanza,
callback=self._async_finished,
timeout=self._timeout)
def _async_finished(self, _client, result, *args, **kwargs):
if self._state == TaskState.CANCELLED:
return
if result is None:
self._error = TimeoutStanzaError()
self._set_finished()
return
self._next_step(result)
def _finalize(self):
if self._iq_id is not None:
self._client._dispatcher.remove_iq_callback(self._iq_id)
self._client = None
super()._finalize()
class HTTPRequestTask(Task):
'''
A Task for running HTTP requests
'''
_process_types = (Soup.Message,)
def __init__(self, gen, logger, session):
super().__init__(gen, logger)
self._session = session
def _run_async(self, message):
self._session.queue_message(message, self._async_finished, None)
def _async_finished(self, _session, message, _user_data):
if self._state != TaskState.CANCELLED:
self._next_step(message)
def _finalize(self):
self._session = None
super()._finalize()
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/tcp.py 0000664 0000000 0000000 00000033545 14130341156 0020316 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from collections import deque
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import GObject
from nbxmpp.const import TCPState
from nbxmpp.const import ConnectionType
from nbxmpp.util import utf8_decode
from nbxmpp.util import convert_tls_error_flags
from nbxmpp.connection import Connection
log = logging.getLogger('nbxmpp.tcp')
READ_BUFFER_SIZE = 8192
class TCPConnection(Connection):
def __init__(self, *args, **kwargs):
Connection.__init__(self, *args, **kwargs)
self._client = Gio.SocketClient.new()
self._client.set_timeout(7)
if self._address.proxy is not None:
self._proxy_resolver = self._address.proxy.get_resolver()
self._client.set_proxy_resolver(self._proxy_resolver)
GObject.Object.connect(self._client, 'event', self._on_event)
self._con = None
self._read_buffer = b''
self._write_queue = deque([])
self._write_stanza_buffer = None
self._connect_cancellable = Gio.Cancellable()
self._read_cancellable = Gio.Cancellable()
self._tls_handshake_in_progress = False
self._input_closed = False
self._output_closed = False
self._keepalive_id = None
def connect(self):
self.state = TCPState.CONNECTING
if self._address.is_service:
self._client.connect_to_service_async(self._address.domain,
self._address.service,
self._connect_cancellable,
self._on_connect_finished,
None)
elif self._address.is_host:
self._client.connect_to_host_async(self._address.host,
0,
self._connect_cancellable,
self._on_connect_finished,
None)
else:
raise ValueError('Invalid Address')
def _on_event(self, _socket_client, event, _connectable, connection):
if event == Gio.SocketClientEvent.CONNECTING:
self._remote_address = connection.get_remote_address().to_string()
use_proxy = self._address.proxy is not None
target = 'proxy' if use_proxy else self._address.domain
self._log.info('Connecting to %s (%s)',
target,
self._remote_address)
def _check_certificate(self, _connection, certificate, errors):
self._peer_certificate = certificate
self._peer_certificate_errors = convert_tls_error_flags(errors)
if self._accept_certificate():
return True
self.notify('bad-certificate')
return False
def _on_certificate_set(self, connection, _param):
self._peer_certificate = connection.props.peer_certificate
self._peer_certificate_errors = convert_tls_error_flags(
connection.props.peer_certificate_errors)
self._tls_handshake_in_progress = False
self.notify('certificate-set')
def _on_connect_finished(self, client, result, _user_data):
try:
if self._address.proxy is not None:
self._con = client.connect_to_host_finish(result)
elif self._address.is_service:
self._con = client.connect_to_service_finish(result)
elif self._address.is_host:
self._con = client.connect_to_host_finish(result)
else:
raise ValueError('Address must be a service or host')
except GLib.Error as error:
self._log.info('Connect Error: %s', error)
self._finalize('connection-failed')
return
# We use the timeout only for connecting
self._con.get_socket().set_timeout(0)
self._con.set_graceful_disconnect(True)
self._con.get_socket().set_keepalive(True)
self._local_address = self._con.get_local_address()
self.state = TCPState.CONNECTED
use_proxy = self._address.proxy is not None
target = 'proxy' if use_proxy else self._address.domain
self._log.info('Connected to %s (%s)',
target,
self._con.get_remote_address().to_string())
self._on_connected()
def _on_connected(self):
self.notify('connected')
self._read_async()
def _remove_keepalive_timer(self):
if self._keepalive_id is not None:
self._log.info('Remove keepalive timer')
GLib.source_remove(self._keepalive_id)
self._keepalive_id = None
def _renew_keepalive_timer(self):
if self._con is None:
return
self._remove_keepalive_timer()
self._log.info('Add keepalive timer')
self._keepalive_id = GLib.timeout_add_seconds(5, self._send_keepalive)
def _send_keepalive(self):
self._log.info('Send keepalive')
self._keepalive_id = None
if not self._con.get_output_stream().has_pending():
self._write_all_async(' '.encode())
def start_tls_negotiation(self):
self._log.info('Start TLS negotiation')
self._tls_handshake_in_progress = True
remote_address = self._con.get_remote_address()
identity = Gio.NetworkAddress.new(self._address.domain,
remote_address.props.port)
tls_client = Gio.TlsClientConnection.new(self._con, identity)
if self._address.type == ConnectionType.DIRECT_TLS:
tls_client.set_advertised_protocols(['xmpp-client'])
tls_client.set_validation_flags(Gio.TlsCertificateFlags.VALIDATE_ALL)
tls_client.connect('accept-certificate', self._check_certificate)
tls_client.connect('notify::peer-certificate', self._on_certificate_set)
# This Wraps the Gio.TlsClientConnection and the Gio.Socket together
# so we get back a Gio.SocketConnection
self._con = Gio.TcpWrapperConnection.new(tls_client,
self._con.get_socket())
def _read_async(self):
if self._input_closed:
return
self._con.get_input_stream().read_bytes_async(
READ_BUFFER_SIZE,
GLib.PRIORITY_DEFAULT,
self._read_cancellable,
self._on_read_async_finish,
None)
def _on_read_async_finish(self, stream, result, _user_data):
try:
data = stream.read_bytes_finish(result)
except GLib.Error as error:
quark = GLib.quark_try_string('g-io-error-quark')
if error.matches(quark, Gio.IOErrorEnum.CANCELLED):
if self._input_closed:
return
quark = GLib.quark_try_string('g-tls-error-quark')
if error.matches(quark, Gio.TlsError.MISC):
if self._tls_handshake_in_progress:
self._log.error('Handshake failed: %s', error)
self._finalize('connection-failed')
return
if error.matches(quark, Gio.TlsError.EOF):
self._log.info('Incoming stream closed: TLS EOF')
self._finalize('disconnected')
return
if error.matches(quark, Gio.TlsError.BAD_CERTIFICATE):
self._log.info('Certificate Error: %s', error)
self._finalize('disconnected')
return
self._log.error('Read Error: %s', error)
if self._state not in (TCPState.DISCONNECTING,
TCPState.DISCONNECTED):
self._finalize('disconnected')
return
except RuntimeError as error:
# PyGObject raises a RuntimeError when it fails to convert the
# GError. Why it failed is printed by PyGObject
self._log.error(error)
return
data = data.get_data()
if not data:
if self._state == TCPState.DISCONNECTING:
self._log.info('Reveived zero data on _read_async()')
self._finalize('disconnected')
else:
self._log.warning('Reveived zero data on _read_async()')
return
self._renew_keepalive_timer()
self._read_buffer += data
try:
data, self._read_buffer = utf8_decode(self._read_buffer)
except UnicodeDecodeError as error:
self._log.warning(error)
self._log.warning('read buffer: "%s"', self._read_buffer)
self._log.warning('data: "%s"', data)
self._finalize('disconnected')
return
self._log_stanza(data, received=True)
try:
self.notify('data-received', data)
except Exception:
self._log.exception('Error while executing data-received:')
# Call next async read only after the received data is processed
# otherwise this can lead to problems if we call
# start_tls_negotiation() while we have a pending read which is
# the case for START TLS because its triggered by
self._read_async()
def _write_stanzas(self):
self._write_stanza_buffer = self._write_queue
self._write_queue = deque([])
data = ''.join(map(str, self._write_stanza_buffer)).encode()
self._write_all_async(data)
def _write_all_async(self, data):
# We have to pass data to the callback, because GLib takes no
# reference on the passed data and python would gc collect it
# bevor GLib has a chance to write it to the stream
self._con.get_output_stream().write_all_async(
data,
GLib.PRIORITY_DEFAULT,
None,
self._on_write_all_async_finished,
data)
def _on_write_all_async_finished(self, stream, result, data):
try:
stream.write_all_finish(result)
except GLib.Error as error:
quark = GLib.quark_try_string('g-tls-error-quark')
if error.matches(quark, Gio.TlsError.BAD_CERTIFICATE):
self._write_stanza_buffer = None
return
if self._output_closed:
self._check_for_shutdown()
return
self._log.error('Write Error: %s', error)
return
except RuntimeError as error:
# PyGObject raises a RuntimeError when it fails to convert the
# GError. Why it failed is printed by PyGObject
self._log.error(error)
return
data = data.decode()
self._log_stanza(data, received=False)
if data == ' ':
# keepalive whitespace
self._renew_keepalive_timer()
else:
for stanza in self._write_stanza_buffer:
try:
self.notify('data-sent', stanza)
except Exception:
self._log.exception('Error while executing data-sent:')
if self._output_closed and not self._write_queue:
self._check_for_shutdown()
return
if self._write_queue:
self._write_stanzas()
def send(self, stanza, now=False):
if self._state in (TCPState.DISCONNECTED, TCPState.DISCONNECTING):
self._log.warning('send() not possible in state: %s', self._state)
return
if now:
self._write_queue.appendleft(stanza)
else:
self._write_queue.append(stanza)
if not self._con.get_output_stream().has_pending():
self._write_stanzas()
def disconnect(self):
self._remove_keepalive_timer()
if self.state == TCPState.CONNECTING:
self.state = TCPState.DISCONNECTING
self._connect_cancellable.cancel()
return
if self._state in (TCPState.DISCONNECTED, TCPState.DISCONNECTING):
self._log.warning('Called disconnect on state: %s', self._state)
return
self.state = TCPState.DISCONNECTING
self._finalize('disconnected')
def _check_for_shutdown(self):
if self._input_closed and self._output_closed:
self._finalize('disconnected')
def shutdown_input(self):
self._remove_keepalive_timer()
self._log.info('Shutdown input')
self._input_closed = True
self._read_cancellable.cancel()
self._check_for_shutdown()
def shutdown_output(self):
self._remove_keepalive_timer()
self.state = TCPState.DISCONNECTING
self._log.info('Shutdown output')
self._output_closed = True
def _finalize(self, signal_name):
self._remove_keepalive_timer()
if self._con is not None:
try:
self._con.get_socket().shutdown(True, True)
except GLib.Error as error:
self._log.info(error)
self._input_closed = True
self._output_closed = True
self.state = TCPState.DISCONNECTED
self.notify(signal_name)
self.destroy()
def destroy(self):
super().destroy()
self._con = None
self._client = None
self._write_queue = None
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/third_party/ 0000775 0000000 0000000 00000000000 14130341156 0021475 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/third_party/__init__.py 0000664 0000000 0000000 00000000000 14130341156 0023574 0 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/nbxmpp/third_party/hsluv.py 0000664 0000000 0000000 00000021307 14130341156 0023213 0 ustar 00root root 0000000 0000000 # This file was taken from https://github.com/hsluv/hsluv-python
#
# Copyright (c) 2015 Alexei Boronine
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import math
m = [[3.240969941904521, -1.537383177570093, -0.498610760293],
[-0.96924363628087, 1.87596750150772, 0.041555057407175],
[0.055630079696993, -0.20397695888897, 1.056971514242878]]
minv = [[0.41239079926595, 0.35758433938387, 0.18048078840183],
[0.21263900587151, 0.71516867876775, 0.072192315360733],
[0.019330818715591, 0.11919477979462, 0.95053215224966]]
refY = 1.0
refU = 0.19783000664283
refV = 0.46831999493879
kappa = 903.2962962
epsilon = 0.0088564516
hex_chars = "0123456789abcdef"
def _distance_line_from_origin(line):
v = math.pow(line['slope'], 2) + 1
return math.fabs(line['intercept']) / math.sqrt(v)
def _length_of_ray_until_intersect(theta, line):
return line['intercept'] / (math.sin(theta) -
line['slope'] * math.cos(theta))
def _get_bounds(l):
result = []
sub1 = math.pow(l + 16, 3) / 1560896
if sub1 > epsilon:
sub2 = sub1
else:
sub2 = l / kappa
_g = 0
while _g < 3:
c = _g
_g = _g + 1
m1 = m[c][0]
m2 = m[c][1]
m3 = m[c][2]
_g1 = 0
while _g1 < 2:
t = _g1
_g1 = _g1 + 1
top1 = (284517 * m1 - 94839 * m3) * sub2
top2 = ((838422 * m3 + 769860 * m2 + 731718 * m1) *
l * sub2 - (769860 * t) * l)
bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t
result.append({'slope': top1 / bottom, 'intercept': top2 / bottom})
return result
def _max_safe_chroma_for_l(l):
bounds = _get_bounds(l)
_hx_min = 1.7976931348623157e+308
_g = 0
while _g < 2:
i = _g
_g = _g + 1
length = _distance_line_from_origin(bounds[i])
if math.isnan(_hx_min):
pass
elif math.isnan(length):
_hx_min = length
else:
_hx_min = min(_hx_min, length)
return _hx_min
def _max_chroma_for_lh(l, h):
hrad = h / 360 * math.pi * 2
bounds = _get_bounds(l)
_hx_min = 1.7976931348623157e+308
_g = 0
while _g < len(bounds):
bound = bounds[_g]
_g = (_g + 1)
length = _length_of_ray_until_intersect(hrad, bound)
if length >= 0:
if math.isnan(_hx_min):
pass
elif math.isnan(length):
_hx_min = length
else:
_hx_min = min(_hx_min, length)
return _hx_min
def _dot_product(a, b):
sum = 0
_g1 = 0
_g = len(a)
while _g1 < _g:
i = _g1
_g1 = _g1 + 1
sum += a[i] * b[i]
return sum
def _from_linear(c):
if c <= 0.0031308:
return 12.92 * c
return 1.055 * math.pow(c, 0.416666666666666685) - 0.055
def _to_linear(c):
if c > 0.04045:
return math.pow((c + 0.055) / 1.055, 2.4)
return c / 12.92
def xyz_to_rgb(_hx_tuple):
return [
_from_linear(_dot_product(m[0], _hx_tuple)),
_from_linear(_dot_product(m[1], _hx_tuple)),
_from_linear(_dot_product(m[2], _hx_tuple))]
def rgb_to_xyz(_hx_tuple):
rgbl = [_to_linear(_hx_tuple[0]),
_to_linear(_hx_tuple[1]),
_to_linear(_hx_tuple[2])]
return [_dot_product(minv[0], rgbl),
_dot_product(minv[1], rgbl),
_dot_product(minv[2], rgbl)]
def _y_to_l(y):
if y <= epsilon:
return y / refY * kappa
return 116 * math.pow(y / refY, 0.333333333333333315) - 16
def _l_to_y(l):
if l <= 8:
return refY * l / kappa
return refY * math.pow((l + 16) / 116, 3)
def xyz_to_luv(_hx_tuple):
x = float(_hx_tuple[0])
y = float(_hx_tuple[1])
z = float(_hx_tuple[2])
divider = x + 15 * y + 3 * z
var_u = 4 * x
var_v = 9 * y
if divider != 0:
var_u = var_u / divider
var_v = var_v / divider
else:
var_u = float("nan")
var_v = float("nan")
l = _y_to_l(y)
if l == 0:
return [0, 0, 0]
u = 13 * l * (var_u - refU)
v = 13 * l * (var_v - refV)
return [l, u, v]
def luv_to_xyz(_hx_tuple):
l = float(_hx_tuple[0])
u = float(_hx_tuple[1])
v = float(_hx_tuple[2])
if l == 0:
return [0, 0, 0]
var_u = u / (13 * l) + refU
var_v = v / (13 * l) + refV
y = _l_to_y(l)
x = 0 - ((9 * y * var_u) / (((var_u - 4) * var_v) - var_u * var_v))
z = (((9 * y) - (15 * var_v * y)) - (var_v * x)) / (3 * var_v)
return [x, y, z]
def luv_to_lch(_hx_tuple):
l = float(_hx_tuple[0])
u = float(_hx_tuple[1])
v = float(_hx_tuple[2])
_v = (u * u) + (v * v)
if _v < 0:
c = float("nan")
else:
c = math.sqrt(_v)
if c < 0.00000001:
h = 0
else:
hrad = math.atan2(v, u)
h = hrad * 180.0 / 3.1415926535897932
if h < 0:
h = 360 + h
return [l, c, h]
def lch_to_luv(_hx_tuple):
l = float(_hx_tuple[0])
c = float(_hx_tuple[1])
h = float(_hx_tuple[2])
hrad = h / 360.0 * 2 * math.pi
u = math.cos(hrad) * c
v = math.sin(hrad) * c
return [l, u, v]
def hsluv_to_lch(_hx_tuple):
h = float(_hx_tuple[0])
s = float(_hx_tuple[1])
l = float(_hx_tuple[2])
if l > 99.9999999:
return [100, 0, h]
if l < 0.00000001:
return [0, 0, h]
_hx_max = _max_chroma_for_lh(l, h)
c = _hx_max / 100 * s
return [l, c, h]
def lch_to_hsluv(_hx_tuple):
l = float(_hx_tuple[0])
c = float(_hx_tuple[1])
h = float(_hx_tuple[2])
if l > 99.9999999:
return [h, 0, 100]
if l < 0.00000001:
return [h, 0, 0]
_hx_max = _max_chroma_for_lh(l, h)
s = c / _hx_max * 100
return [h, s, l]
def hpluv_to_lch(_hx_tuple):
h = float(_hx_tuple[0])
s = float(_hx_tuple[1])
l = float(_hx_tuple[2])
if l > 99.9999999:
return [100, 0, h]
if l < 0.00000001:
return [0, 0, h]
_hx_max = _max_safe_chroma_for_l(l)
c = _hx_max / 100 * s
return [l, c, h]
def lch_to_hpluv(_hx_tuple):
l = float(_hx_tuple[0])
c = float(_hx_tuple[1])
h = float(_hx_tuple[2])
if l > 99.9999999:
return [h, 0, 100]
if l < 0.00000001:
return [h, 0, 0]
_hx_max = _max_safe_chroma_for_l(l)
s = c / _hx_max * 100
return [h, s, l]
def rgb_to_hex(_hx_tuple):
h = "#"
_g = 0
while _g < 3:
i = _g
_g = _g + 1
chan = float(_hx_tuple[i])
c = math.floor(chan * 255 + 0.5)
digit2 = int(c % 16)
digit1 = int((c - digit2) / 16)
h += hex_chars[digit1] + hex_chars[digit2]
return h
def hex_to_rgb(hex):
hex = hex.lower()
ret = []
_g = 0
while _g < 3:
i = _g
_g = _g + 1
index = i * 2 + 1
_hx_str = hex[index]
digit1 = hex_chars.find(_hx_str)
index1 = i * 2 + 2
str1 = hex[index1]
digit2 = hex_chars.find(str1)
n = digit1 * 16 + digit2
ret.append(n / 255.0)
return ret
def lch_to_rgb(_hx_tuple):
return xyz_to_rgb(luv_to_xyz(lch_to_luv(_hx_tuple)))
def rgb_to_lch(_hx_tuple):
return luv_to_lch(xyz_to_luv(rgb_to_xyz(_hx_tuple)))
def hsluv_to_rgb(_hx_tuple):
return lch_to_rgb(hsluv_to_lch(_hx_tuple))
def rgb_to_hsluv(_hx_tuple):
return lch_to_hsluv(rgb_to_lch(_hx_tuple))
def hpluv_to_rgb(_hx_tuple):
return lch_to_rgb(hpluv_to_lch(_hx_tuple))
def rgb_to_hpluv(_hx_tuple):
return lch_to_hpluv(rgb_to_lch(_hx_tuple))
def hsluv_to_hex(_hx_tuple):
return rgb_to_hex(hsluv_to_rgb(_hx_tuple))
def hpluv_to_hex(_hx_tuple):
return rgb_to_hex(hpluv_to_rgb(_hx_tuple))
def hex_to_hsluv(s):
return rgb_to_hsluv(hex_to_rgb(s))
def hex_to_hpluv(s):
return rgb_to_hpluv(hex_to_rgb(s))
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/util.py 0000664 0000000 0000000 00000032077 14130341156 0020504 0 ustar 00root root 0000000 0000000 # Copyright (C) 2018 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import base64
import hashlib
import uuid
import binascii
import os
import re
import logging
from logging import LoggerAdapter
from collections import defaultdict
from functools import lru_cache
from gi.repository import Gio
from nbxmpp.protocol import DiscoInfoMalformed
from nbxmpp.const import GIO_TLS_ERRORS
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import StanzaMalformed
from nbxmpp.protocol import StreamHeader
from nbxmpp.protocol import WebsocketOpenHeader
from nbxmpp.structs import Properties
from nbxmpp.structs import IqProperties
from nbxmpp.structs import MessageProperties
from nbxmpp.structs import PresenceProperties
from nbxmpp.structs import CommonError
from nbxmpp.structs import HTTPUploadError
from nbxmpp.structs import StanzaMalformedError
from nbxmpp.modules.dataforms import extend_form
from nbxmpp.third_party.hsluv import hsluv_to_rgb
log = logging.getLogger('nbxmpp.util')
def b64decode(data, return_type=str):
if not data:
raise ValueError('No data to decode')
if isinstance(data, str):
data = data.encode()
result = base64.b64decode(data)
if return_type == bytes:
return result
return result.decode()
def b64encode(data, return_type=str):
if not data:
raise ValueError('No data to encode')
if isinstance(data, str):
data = data.encode()
result = base64.b64encode(data)
if return_type == bytes:
return result
return result.decode()
def get_properties_struct(name, own_jid):
if name == 'message':
return MessageProperties(own_jid)
if name == 'iq':
return IqProperties(own_jid)
if name == 'presence':
return PresenceProperties(own_jid)
return Properties()
def from_xs_boolean(value):
if value in ('1', 'true', 'True'):
return True
if value in ('0', 'false', 'False', ''):
return False
raise ValueError('Cant convert %s to python boolean' % value)
def to_xs_boolean(value):
# Convert to xs:boolean ('true', 'false')
# from a python boolean (True, False) or None
if value is True:
return 'true'
if value is False:
return 'false'
if value is None:
return 'false'
raise ValueError(
'Cant convert %s to xs:boolean' % value)
error_classes = {
Namespace.HTTPUPLOAD_0: HTTPUploadError
}
def error_factory(stanza, condition=None, text=None):
if condition == 'stanza-malformed':
return StanzaMalformedError(stanza, text)
app_namespace = stanza.getAppErrorNamespace()
return error_classes.get(app_namespace, CommonError)(stanza)
def clip_rgb(red, green, blue):
return (
min(max(red, 0), 1),
min(max(green, 0), 1),
min(max(blue, 0), 1),
)
@lru_cache(maxsize=1024)
def text_to_color(text, background_color):
# background color = (rb, gb, bb)
hash_ = hashlib.sha1()
hash_.update(text.encode())
hue = int.from_bytes(hash_.digest()[:2], 'little') / 65536
red, green, blue = clip_rgb(*hsluv_to_rgb((hue * 360, 100, 50)))
rb, gb, bb = background_color
rb_inv = 1 - rb
gb_inv = 1 - gb
bb_inv = 1 - bb
rc = 0.2 * rb_inv + 0.8 * red
gc = 0.2 * gb_inv + 0.8 * green
bc = 0.2 * bb_inv + 0.8 * blue
return rc, gc, bc
def compute_caps_hash(info, compare=True):
"""
Compute caps hash according to XEP-0115, V1.5
https://xmpp.org/extensions/xep-0115.html#ver-proc
:param: info DiscoInfo
:param: compare If True an exception is raised if the hash announced in
the node attr is not equal to what is calculated
"""
# Initialize an empty string S.
string_ = ''
# Sort the service discovery identities by category and then by type and
# then by xml:lang (if it exists), formatted as
# CATEGORY '/' [TYPE] '/' [LANG] '/' [NAME]. Note that each slash is
# included even if the LANG or NAME is not included (in accordance with
# XEP-0030, the category and type MUST be included).
# For each identity, append the 'category/type/lang/name' to S, followed by
# the '<' character.
# Sort the supported service discovery features.
def sort_identities_key(i):
return (i.category, i.type, i.lang or '')
identities = sorted(info.identities, key=sort_identities_key)
for identity in identities:
string_ += '%s<' % str(identity)
# If the response includes more than one service discovery identity with
# the same category/type/lang/name, consider the entire response
# to be ill-formed.
if len(set(identities)) != len(identities):
raise DiscoInfoMalformed('Non-unique identity found')
# Sort the supported service discovery features.
# For each feature, append the feature to S, followed by the '<' character.
features = sorted(info.features)
for feature in features:
string_ += '%s<' % feature
# If the response includes more than one service discovery feature with the
# same XML character data, consider the entire response to be ill-formed.
if len(set(features)) != len(features):
raise DiscoInfoMalformed('Non-unique feature found')
# If the response includes more than one extended service discovery
# information form with the same FORM_TYPE or the FORM_TYPE field contains
# more than one element with different XML character data,
# consider the entire response to be ill-formed.
# If the response includes an extended service discovery information form
# where the FORM_TYPE field is not of type "hidden" or the form does not
# include a FORM_TYPE field, ignore the form but continue processing.
dataforms = []
form_type_values = []
for dataform in info.dataforms:
form_type = dataform.vars.get('FORM_TYPE')
if form_type is None:
# Ignore dataform because of missing FORM_TYPE
continue
if form_type.type_ != 'hidden':
# Ignore dataform because of wrong type
continue
values = form_type.getTags('value')
if len(values) != 1:
raise DiscoInfoMalformed('Form should have exactly '
'one FORM_TYPE value')
value = values[0].getData()
dataforms.append(dataform)
form_type_values.append(value)
if len(set(form_type_values)) != len(form_type_values):
raise DiscoInfoMalformed('Non-unique FORM_TYPE value found')
# If the service discovery information response includes XEP-0128 data
# forms, sort the forms by the FORM_TYPE (i.e., by the XML character data
# of the element).
# For each extended service discovery information form:
# - Append the XML character data of the FORM_TYPE field's
# element, followed by the '<' character.
# - Sort the fields by the value of the "var" attribute.
# - For each field other than FORM_TYPE:
# - Append the value of the "var" attribute, followed by the
# '<' character.
# - Sort values by the XML character data of the element.
# - For each element, append the XML character data,
# followed by the '<' character.
def sort_dataforms_key(dataform):
return dataform['FORM_TYPE'].getTagData('value')
dataforms = sorted(dataforms, key=sort_dataforms_key)
for dataform in dataforms:
string_ += '%s<' % dataform['FORM_TYPE'].getTagData('value')
fields = {}
for field in dataform.iter_fields():
if field.var == 'FORM_TYPE':
continue
values = field.getTags('value')
fields[field.var] = sorted([value.getData() for value in values])
for var in sorted(fields.keys()):
string_ += '%s<' % var
for value in fields[var]:
string_ += '%s<' % value
hash_ = hashlib.sha1(string_.encode())
b64hash = b64encode(hash_.digest())
if compare and b64hash != info.get_caps_hash():
raise DiscoInfoMalformed('Caps hashes differ: %s != %s' % (
b64hash, info.get_caps_hash()))
return b64hash
def generate_id():
return str(uuid.uuid4())
def get_form(stanza, form_type):
forms = stanza.getTags('x', namespace=Namespace.DATA)
if not forms:
return None
for form in forms:
form = extend_form(node=form)
field = form.vars.get('FORM_TYPE')
if field is None:
continue
if field.value == form_type:
return form
return None
def validate_stream_header(stanza, domain, is_websocket):
attrs = stanza.getAttrs()
if attrs.get('from') != domain:
raise StanzaMalformed('Invalid from attr in stream header')
if is_websocket:
if attrs.get('xmlns') != Namespace.FRAMING:
raise StanzaMalformed('Invalid namespace in stream header')
else:
if attrs.get('xmlns:stream') != Namespace.STREAMS:
raise StanzaMalformed('Invalid stream namespace in stream header')
if attrs.get('xmlns') != Namespace.CLIENT:
raise StanzaMalformed('Invalid namespace in stream header')
if attrs.get('version') != '1.0':
raise StanzaMalformed('Invalid stream version in stream header')
stream_id = attrs.get('id')
if stream_id is None:
raise StanzaMalformed('No stream id found in stream header')
return stream_id
def get_stream_header(domain, lang, is_websocket):
if is_websocket:
return WebsocketOpenHeader(domain, lang)
header = StreamHeader(domain, lang)
return "%s>" % str(header)[:-3]
def get_stanza_id():
return str(uuid.uuid4())
def utf8_decode(data):
'''
Decodes utf8 byte string to unicode string
Does handle invalid utf8 sequences by splitting
the invalid sequence at the end
returns (decoded unicode string, invalid byte sequence)
'''
try:
return data.decode(), b''
except UnicodeDecodeError:
for i in range(-1, -4, -1):
char = data[i]
if char & 0xc0 == 0x80:
continue
return data[:i].decode(), data[i:]
raise
def get_rand_number():
return int(binascii.hexlify(os.urandom(6)), 16)
def get_invalid_xml_regex():
# \ufddo -> \ufdef range
c = '\ufdd0'
r = c
while c < '\ufdef':
c = chr(ord(c) + 1)
r += '|' + c
# \ufffe-\uffff, \u1fffe-\u1ffff, ..., \u10fffe-\u10ffff
c = '\ufffe'
r += '|' + c
r += '|' + chr(ord(c) + 1)
while c < '\U0010fffe':
c = chr(ord(c) + 0x10000)
r += '|' + c
r += '|' + chr(ord(c) + 1)
return re.compile(r)
def get_tls_error_phrase(tls_error):
phrase = GIO_TLS_ERRORS.get(tls_error)
if phrase is None:
return GIO_TLS_ERRORS.get(Gio.TlsCertificateFlags.GENERIC_ERROR)
return phrase
def convert_tls_error_flags(flags):
if not flags:
return set()
# If GLib ever adds more flags GIO_TLS_ERRORS have to
# be extended, otherwise errors go unnoticed
if Gio.TlsCertificateFlags.VALIDATE_ALL != 127:
raise ValueError
return set(filter(lambda error: error & flags, GIO_TLS_ERRORS.keys()))
def get_websocket_close_string(websocket):
data = websocket.get_close_data()
code = websocket.get_close_code()
if code is None and data is None:
return ''
return ' Data: %s Code: %s' % (data, code)
def is_websocket_close(stanza):
return (stanza.getName() == 'close' and
stanza.getNamespace() == Namespace.FRAMING)
def is_websocket_stream_error(stanza):
return (stanza.getName() == 'error' and
stanza.getNamespace() == Namespace.STREAMS)
class Observable:
def __init__(self, log_):
self._log = log_
self._frozen = False
self._callbacks = defaultdict(list)
def remove_subscriptions(self):
self._callbacks = defaultdict(list)
def subscribe(self, signal_name, func):
self._callbacks[signal_name].append(func)
def notify(self, signal_name, *args, **kwargs):
if self._frozen:
self._frozen = False
return
self._log.info('Signal: %s', signal_name)
callbacks = self._callbacks.get(signal_name, [])
for func in callbacks:
func(self, signal_name, *args, **kwargs)
class LogAdapter(LoggerAdapter):
def set_context(self, context):
self.extra['context'] = context
def process(self, msg, kwargs):
return '(%s) %s' % (self.extra['context'], msg), kwargs
python-nbxmpp-nbxmpp-2.0.4/nbxmpp/websocket.py 0000664 0000000 0000000 00000014410 14130341156 0021504 0 ustar 00root root 0000000 0000000 # Copyright (C) 2020 Philipp Hörist
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see .
import logging
from gi.repository import Soup
from gi.repository import GLib
from gi.repository import Gio
from nbxmpp.const import TCPState
from nbxmpp.const import ConnectionType
from nbxmpp.util import get_websocket_close_string
from nbxmpp.util import convert_tls_error_flags
from nbxmpp.connection import Connection
log = logging.getLogger('nbxmpp.websocket')
class WebsocketConnection(Connection):
def __init__(self, *args, **kwargs):
Connection.__init__(self, *args, **kwargs)
self._session = Soup.Session()
self._session.props.ssl_strict = False
if self._log.getEffectiveLevel() == logging.INFO:
self._session.add_feature(
Soup.Logger.new(Soup.LoggerLogLevel.BODY, -1))
self._websocket = None
self._cancellable = Gio.Cancellable()
self._input_closed = False
self._output_closed = False
def connect(self):
self._log.info('Try to connect to %s', self._address.uri)
self.state = TCPState.CONNECTING
message = Soup.Message.new('GET', self._address.uri)
message.connect('starting', self._check_certificate)
message.set_flags(Soup.MessageFlags.NO_REDIRECT)
self._session.websocket_connect_async(message,
None,
['xmpp'],
self._cancellable,
self._on_connect,
None)
def _on_connect(self, session, result, _user_data):
# TODO: check if protocol 'xmpp' is set
try:
self._websocket = session.websocket_connect_finish(result)
except GLib.Error as error:
quark = GLib.quark_try_string('g-io-error-quark')
if error.matches(quark, Gio.IOErrorEnum.CANCELLED):
self._finalize('disconnected')
return
self._log.info('Connection Error: %s', error)
self._finalize('connection-failed')
return
self._websocket.set_keepalive_interval(5)
self._websocket.connect('message', self._on_websocket_message)
self._websocket.connect('closed', self._on_websocket_closed)
self._websocket.connect('closing', self._on_websocket_closing)
self._websocket.connect('error', self._on_websocket_error)
self._websocket.connect('pong', self._on_websocket_pong)
self.state = TCPState.CONNECTED
self.notify('connected')
def start_tls_negotiation(self):
# Soup.Session does this automatically
raise NotImplementedError
def _check_certificate(self, message):
https_used, certificate, errors = message.get_https_status()
if not https_used and self._address.type == ConnectionType.PLAIN:
return
self._peer_certificate = certificate
self._peer_certificate_errors = convert_tls_error_flags(errors)
self.notify('certificate-set')
if self._accept_certificate():
return
self.notify('bad-certificate')
self._cancellable.cancel()
def _on_websocket_message(self, _websocket, _type, message):
data = message.get_data().decode()
self._log_stanza(data)
if self._input_closed:
self._log.warning('Received data after stream closed')
return
self.notify('data-received', data)
def _on_websocket_pong(self, _websocket, _message):
self._log.info('Pong received')
def _on_websocket_closed(self, websocket):
self._log.info('Closed %s', get_websocket_close_string(websocket))
self._finalize('disconnected')
def _on_websocket_closing(self, _websocket):
self._log.info('Closing')
def _on_websocket_error(self, _websocket, error):
self._log.error(error)
if self._state not in (TCPState.DISCONNECTED, TCPState.DISCONNECTING):
self._finalize('disconnected')
def send(self, stanza, now=False):
if self._state in (TCPState.DISCONNECTED, TCPState.DISCONNECTING):
self._log.warning('send() not possible in state: %s', self._state)
return
data = str(stanza)
self._websocket.send_text(data)
self._log_stanza(data, received=False)
self.notify('data-sent', stanza)
def disconnect(self):
if self._state == TCPState.CONNECTING:
self.state = TCPState.DISCONNECTING
self._cancellable.cancel()
return
if self._state in (TCPState.DISCONNECTED, TCPState.DISCONNECTING):
self._log.warning('Called disconnect on state: %s', self._state)
return
self._websocket.close(Soup.WebsocketCloseCode.NORMAL, None)
self.state = TCPState.DISCONNECTING
def _check_for_shutdown(self):
if self._input_closed and self._output_closed:
self._websocket.close(Soup.WebsocketCloseCode.NORMAL, None)
def shutdown_input(self):
self._log.info('Shutdown input')
self._input_closed = True
self._check_for_shutdown()
def shutdown_output(self):
self.state = TCPState.DISCONNECTING
self._log.info('Shutdown output')
self._output_closed = True
def _finalize(self, signal_name):
self._input_closed = True
self._output_closed = True
self.state = TCPState.DISCONNECTED
self.notify(signal_name)
self.destroy()
def destroy(self):
super().destroy()
self._session.abort()
self._session = None
self._websocket = None
python-nbxmpp-nbxmpp-2.0.4/python-nbxmpp.doap 0000664 0000000 0000000 00000034554 14130341156 0021343 0 ustar 00root root 0000000 0000000
python-nbxmppnbxmppXMPP Library
python-nbxmpp is a Python library that provides a way for Python applications to use the XMPP network.
This library was initially a fork of xmpppy.
PythonLinuxWindowscomplete2.9complete1.3complete2.4complete1.31.2All basics stuff is supported. Also, some more advanced features like invitations, creation of rooms, etc.complete1.3complete1.1complete1.2complete1.2complete1.0Used by XEP-0313.partial1.13partial1.5complete1.0complete2.4complete1.9complete1.1complete1.0complete1.1.1complete2.1complete1.1complete1.1.1complete1.2complete1.3complete1.5complete1.2complete1.0complete1.0partial1.0Images only.complete1.0complete1.2complete1.2complete1.3complete2.0complete1.0partial1.0No cache: Only for captchas.complete1.0.0complete1.0complete0.12.0complete0.10complete1.0complete0.6.3complete1.0.2complete0.2.1complete0.7.0complete1.0.0complete0.4.0partial0.2Basic Spam/Abuse reporting via XEP-0191complete0.1complete0.1complete0.6.0complete0.3.0
python-nbxmpp-nbxmpp-2.0.4/setup.cfg 0000664 0000000 0000000 00000001036 14130341156 0017461 0 ustar 00root root 0000000 0000000 [metadata]
name = nbxmpp
version = 2.0.4
description = XMPP Library
author = Yann Leboulanger, Philipp Hoerist
author_email = gajim-devel@gajim.org
long_description = file: README.md
long_description_content_type = text/markdown
keywords = xmpp chat
url = https://dev.gajim.org/gajim/python-nbxmpp
license_file = COPYING
license = GPL v3
classifiers =
Programming Language :: Python :: 3
[options]
python_requires = >=3.7
packages = find:
install_requires =
precis-i18n>=1.0.0
idna
[options.packages.find]
exclude =
test*
python-nbxmpp-nbxmpp-2.0.4/setup.py 0000775 0000000 0000000 00000000076 14130341156 0017360 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from setuptools import setup
setup()
python-nbxmpp-nbxmpp-2.0.4/test/ 0000775 0000000 0000000 00000000000 14130341156 0016617 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/test/__init__.py 0000664 0000000 0000000 00000000164 14130341156 0020731 0 ustar 00root root 0000000 0000000 import logging
# Prevents logging output in tests
log = logging.getLogger('nbxmpp')
log.setLevel(logging.CRITICAL)
python-nbxmpp-nbxmpp-2.0.4/test/lib/ 0000775 0000000 0000000 00000000000 14130341156 0017365 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/test/lib/__init__.py 0000664 0000000 0000000 00000001505 14130341156 0021477 0 ustar 00root root 0000000 0000000 def xml2str_sorted(data):
s = "<" + data.name
if data.namespace:
if not data.parent or data.parent.namespace!=data.namespace:
if 'xmlns' not in data.attrs:
s += ' xmlns="%s"' % data.namespace
for key in sorted(data.attrs.keys()):
val = str(data.attrs[key])
s += ' %s="%s"' % (key, val)
s += ">"
cnt = 0
if data.kids:
for a in data.kids:
if (len(data.data)-1) >= cnt:
s += data.data[cnt]
if isinstance(a, str):
s += a.__str__()
else:
s += xml2str_sorted(a)
cnt += 1
if (len(data.data)-1) >= cnt:
s += data.data[cnt]
if not data.kids and s.endswith('>'):
s = s[:-1] + ' />'
else:
s += "" + data.name + ">"
return s
python-nbxmpp-nbxmpp-2.0.4/test/lib/const.py 0000664 0000000 0000000 00000000150 14130341156 0021061 0 ustar 00root root 0000000 0000000
STREAM_START = ""
python-nbxmpp-nbxmpp-2.0.4/test/lib/util.py 0000664 0000000 0000000 00000001064 14130341156 0020715 0 ustar 00root root 0000000 0000000 import unittest
from unittest.mock import Mock
from test.lib.const import STREAM_START
from nbxmpp.dispatcher import StanzaDispatcher
from nbxmpp.protocol import JID
class StanzaHandlerTest(unittest.TestCase):
def setUp(self):
# Setup mock client
self.client = Mock()
self.client.is_websocket = False
self.dispatcher = StanzaDispatcher(self.client)
self.client.get_bound_jid.return_value = JID.from_string('test@test.test')
self.dispatcher.reset_parser()
self.dispatcher.process_data(STREAM_START)
python-nbxmpp-nbxmpp-2.0.4/test/lib/xmpp_mocks.py 0000664 0000000 0000000 00000005222 14130341156 0022120 0 ustar 00root root 0000000 0000000 '''
Module with dummy classes for unit testing of XMPP and related code.
'''
import threading, time
from unittest.mock import Mock
from nbxmpp import idlequeue
IDLEQUEUE_INTERVAL = 0.2 # polling interval. 200ms is used in Gajim as default
IDLEMOCK_TIMEOUT = 30 # how long we wait for an event
class IdleQueueThread(threading.Thread):
'''
Thread for regular processing of idlequeue.
'''
def __init__(self):
self.iq = idlequeue.SelectIdleQueue()
self.stop = threading.Event() # Event to stop the thread main loop.
self.stop.clear()
threading.Thread.__init__(self)
def run(self):
while not self.stop.isSet():
self.iq.process()
time.sleep(IDLEQUEUE_INTERVAL)
def stop_thread(self):
self.stop.set()
class IdleMock:
'''
Serves as template for testing objects that are normally controlled by GUI.
Allows to wait for asynchronous callbacks with wait() method.
'''
def __init__(self):
self._event = threading.Event()
self._event.clear()
def wait(self):
'''
Block until some callback sets the event and clearing the event
subsequently.
Returns True if event was set, False on timeout
'''
self._event.wait(IDLEMOCK_TIMEOUT)
if self._event.isSet():
self._event.clear()
return True
else:
return False
def set_event(self):
self._event.set()
class MockConnection(IdleMock, Mock):
'''
Class simulating Connection class from src/common/connection.py
It is derived from Mock in order to avoid defining all methods
from real Connection that are called from NBClient or Dispatcher
( _event_dispatcher for example)
'''
def __init__(self, *args):
self.connect_succeeded = True
IdleMock.__init__(self)
Mock.__init__(self, *args)
def on_connect(self, success, *args):
'''
Method called after connecting - after receiving
from server (NOT after TLS stream restart) or connect failure
'''
self.connect_succeeded = success
self.set_event()
def on_auth(self, con, auth):
'''
Method called after authentication, regardless of the result.
:Parameters:
con : NonBlockingClient
reference to authenticated object
auth : string
type of authetication in case of success ('old_auth', 'sasl') or
None in case of auth failure
'''
self.auth_connection = con
self.auth = auth
self.set_event()
python-nbxmpp-nbxmpp-2.0.4/test/unit/ 0000775 0000000 0000000 00000000000 14130341156 0017576 5 ustar 00root root 0000000 0000000 python-nbxmpp-nbxmpp-2.0.4/test/unit/__init__.py 0000664 0000000 0000000 00000000066 14130341156 0021711 0 ustar 00root root 0000000 0000000 '''
This package just contains plain unit tests
'''
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_activity.py 0000664 0000000 0000000 00000003667 14130341156 0023057 0 ustar 00root root 0000000 0000000 from test.lib.util import StanzaHandlerTest
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import ActivityData
from nbxmpp.structs import PubSubEventData
class ActivityTest(StanzaHandlerTest):
def test_activity_parsing(self):
def _on_message(_con, _stanza, properties):
data = ActivityData(activity='relaxing',
subactivity='partying',
text='My nurse\'s birthday!')
pubsub_event = PubSubEventData(
node='http://jabber.org/protocol/activity',
id='b5ac48d0-0f9c-11dc-8754-001143d5d5db',
item=None,
data=data,
deleted=False,
retracted=False,
purged=False)
# We cant compare Node objects
pubsub_event_ = properties.pubsub_event._replace(item=None)
self.assertEqual(pubsub_event, pubsub_event_)
event = '''
My nurse's birthday!
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT))
self.dispatcher.process_data(event)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_avatar.py 0000664 0000000 0000000 00000004507 14130341156 0022473 0 ustar 00root root 0000000 0000000 from test.lib.util import StanzaHandlerTest
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import PubSubEventData
from nbxmpp.modules.user_avatar import AvatarMetaData
from nbxmpp.modules.user_avatar import AvatarInfo
class AvatarTest(StanzaHandlerTest):
def test_avatar_parsing(self):
def _on_message(_con, _stanza, properties):
info = AvatarInfo(bytes='12345',
height='64',
width='64',
id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f',
type='image/png',
url='http://avatars.example.org/happy.gif')
metadata = AvatarMetaData(infos=[info])
pubsub_event = PubSubEventData(
node='urn:xmpp:avatar:metadata',
id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f',
item=None,
data=metadata,
deleted=False,
retracted=False,
purged=False)
# We cant compare Node objects
pubsub_event_ = properties.pubsub_event._replace(item=None)
self.assertEqual(pubsub_event, pubsub_event_)
event = '''
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT))
self.dispatcher.process_data(event)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_bookmarks.py 0000664 0000000 0000000 00000010567 14130341156 0023210 0 ustar 00root root 0000000 0000000 from test.lib.util import StanzaHandlerTest
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import BookmarkData
from nbxmpp.structs import PubSubEventData
class BookmarkTest(StanzaHandlerTest):
def test_bookmark_1_parsing(self):
def _on_message(_con, _stanza, properties):
data = [
BookmarkData(jid=JID.from_string('theplay@conference.shakespeare.lit'),
name='The Play\'s the Thing',
autojoin=True,
password='pass',
nick='JC'),
BookmarkData(jid=JID.from_string('second@conference.shakespeare.lit'),
name='Second room',
autojoin=False,
password=None,
nick=None)
]
pubsub_event = PubSubEventData(
node='storage:bookmarks',
id='current',
item=None,
data=data,
deleted=False,
retracted=False,
purged=False)
# We cant compare Node objects
pubsub_event_ = properties.pubsub_event._replace(item=None)
self.assertEqual(pubsub_event, pubsub_event_)
event = '''
passJC
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT))
self.dispatcher.process_data(event)
def test_bookmark_2_parsing(self):
def _on_message(_con, _stanza, properties):
data = BookmarkData(jid=JID.from_string('theplay@conference.shakespeare.lit'),
name='The Play\'s the Thing',
autojoin=True,
password=None,
nick='JC')
pubsub_event = PubSubEventData(
node='urn:xmpp:bookmarks:0',
id='theplay@conference.shakespeare.lit',
item=None,
data=data,
deleted=False,
retracted=False,
purged=False)
# We cant compare Node objects
pubsub_event_ = properties.pubsub_event._replace(item=None)
self.assertEqual(pubsub_event, pubsub_event_)
event = '''
JC
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT))
self.dispatcher.process_data(event)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_datetime_parsing.py 0000664 0000000 0000000 00000010003 14130341156 0024520 0 ustar 00root root 0000000 0000000 import unittest
from datetime import datetime
from datetime import timezone
from datetime import timedelta
from nbxmpp.modules.date_and_time import parse_datetime
from nbxmpp.modules.date_and_time import LocalTimezone
from nbxmpp.modules.date_and_time import create_tzinfo
class TestDateTime(unittest.TestCase):
def test_convert_to_utc(self):
strings = {
# Valid UTC strings and fractions
'2017-11-05T01:41:20Z': 1509846080.0,
'2017-11-05T01:41:20.123Z': 1509846080.123,
'2017-11-05T01:41:20.123123123+00:00': 1509846080.123123,
'2017-11-05T01:41:20.123123123123123-00:00': 1509846080.123123,
# Invalid strings
'2017-11-05T01:41:20Z+05:00': None,
'2017-11-05T01:41:20+0000': None,
'2017-11-05T01:41:20-0000': None,
# Valid strings with offset
'2017-11-05T01:41:20-05:00': 1509864080.0,
'2017-11-05T01:41:20+05:00': 1509828080.0,
}
strings2 = {
# Valid strings with offset
'2017-11-05T01:41:20-05:00': datetime(2017, 11, 5, 1, 41, 20, 0, create_tzinfo(hours=-5)),
'2017-11-05T01:41:20+05:00': datetime(2017, 11, 5, 1, 41, 20, 0, create_tzinfo(hours=5)),
}
for time_string, expected_value in strings.items():
result = parse_datetime(time_string, convert='utc', epoch=True)
self.assertEqual(result, expected_value)
for time_string, expected_value in strings2.items():
result = parse_datetime(time_string, convert='utc')
self.assertEqual(result, expected_value.astimezone(timezone.utc))
def test_convert_to_local(self):
strings = {
# Valid UTC strings and fractions
'2017-11-05T01:41:20Z': datetime(2017, 11, 5, 1, 41, 20, 0, timezone.utc),
'2017-11-05T01:41:20.123Z': datetime(2017, 11, 5, 1, 41, 20, 123000, timezone.utc),
'2017-11-05T01:41:20.123123123+00:00': datetime(2017, 11, 5, 1, 41, 20, 123123, timezone.utc),
'2017-11-05T01:41:20.123123123123123-00:00': datetime(2017, 11, 5, 1, 41, 20, 123123, timezone.utc),
# Valid strings with offset
'2017-11-05T01:41:20-05:00': datetime(2017, 11, 5, 1, 41, 20, 0, create_tzinfo(hours=-5)),
'2017-11-05T01:41:20+05:00': datetime(2017, 11, 5, 1, 41, 20, 0, create_tzinfo(hours=5)),
}
for time_string, expected_value in strings.items():
result = parse_datetime(time_string, convert='local')
self.assertEqual(result, expected_value.astimezone(LocalTimezone()))
def test_no_convert(self):
strings = {
# Valid UTC strings and fractions
'2017-11-05T01:41:20Z': timedelta(0),
'2017-11-05T01:41:20.123Z': timedelta(0),
'2017-11-05T01:41:20.123123123+00:00': timedelta(0),
'2017-11-05T01:41:20.123123123123123-00:00': timedelta(0),
# Valid strings with offset
'2017-11-05T01:41:20-05:00': timedelta(hours=-5),
'2017-11-05T01:41:20+05:00': timedelta(hours=5),
}
for time_string, expected_value in strings.items():
result = parse_datetime(time_string, convert=None)
self.assertEqual(result.utcoffset(), expected_value)
def test_check_utc(self):
strings = {
# Valid UTC strings and fractions
'2017-11-05T01:41:20Z': 1509846080.0,
'2017-11-05T01:41:20.123Z': 1509846080.123,
'2017-11-05T01:41:20.123123123+00:00': 1509846080.123123,
'2017-11-05T01:41:20.123123123123123-00:00': 1509846080.123123,
# Valid strings with offset
'2017-11-05T01:41:20-05:00': None,
'2017-11-05T01:41:20+05:00': None,
}
for time_string, expected_value in strings.items():
result = parse_datetime(
time_string, check_utc=True, epoch=True)
self.assertEqual(result, expected_value)
if __name__ == '__main__':
unittest.main()
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_delay_parsing.py 0000664 0000000 0000000 00000001765 14130341156 0024041 0 ustar 00root root 0000000 0000000 import unittest
from nbxmpp.protocol import Node
from nbxmpp.modules.delay import parse_delay
class TestHelpers(unittest.TestCase):
def test_parse_delay(self):
node = """
"""
message = Node(node=node)
timestamp = parse_delay(message)
self.assertEqual(timestamp, 1031699305.0)
timestamp = parse_delay(message, from_=['capulet.com'])
self.assertEqual(timestamp, 1031699305.0)
timestamp = parse_delay(message, from_=['romeo.com'])
self.assertEqual(timestamp, 1284160105.0)
timestamp = parse_delay(message, not_from=['romeo.com'])
self.assertEqual(timestamp, 1031699305.0)
if __name__ == '__main__':
unittest.main()
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_entity_caps.py 0000664 0000000 0000000 00000021066 14130341156 0023536 0 ustar 00root root 0000000 0000000 import unittest
from nbxmpp.util import compute_caps_hash
from nbxmpp.modules.discovery import parse_disco_info
from nbxmpp.protocol import Iq
from nbxmpp.protocol import DiscoInfoMalformed
class EntityCaps(unittest.TestCase):
def test_multiple_field_values(self):
node = """
urn:xmpp:dataforms:softwareinfoipv4ipv6Mac10.5.1Psi0.11"""
info = parse_disco_info(Iq(node=node))
hash_ = compute_caps_hash(info)
self.assertEqual(hash_, 'q07IKJEyjvHSyhy//CH0CxmKi8w=')
def test_ignore_invalid_forms(self):
node = """
urn:xmpp:dataforms:softwareinfoipv4ipv6Mac10.5.1Psi0.11urn:xmpp:dataforms:softwareinfoipv4ipv6ipv4ipv6"""
info = parse_disco_info(Iq(node=node))
hash_ = compute_caps_hash(info)
self.assertEqual(hash_, 'q07IKJEyjvHSyhy//CH0CxmKi8w=')
def test_multiple_form_type_values(self):
node = """
urn:xmpp:dataforms:softwareinfourn:xmpp:dataforms:softwareinfo_testipv4ipv6"""
info = parse_disco_info(Iq(node=node))
with self.assertRaises(DiscoInfoMalformed):
hash_ = compute_caps_hash(info)
def test_non_unique_form_type_value(self):
node = """
urn:xmpp:dataforms:softwareinfoipv4ipv6urn:xmpp:dataforms:softwareinfoipv4ipv6"""
info = parse_disco_info(Iq(node=node))
with self.assertRaises(DiscoInfoMalformed):
hash_ = compute_caps_hash(info)
def test_non_unique_feature(self):
node = """
"""
info = parse_disco_info(Iq(node=node))
with self.assertRaises(DiscoInfoMalformed):
hash_ = compute_caps_hash(info)
def test_non_unique_identity(self):
node = """
"""
info = parse_disco_info(Iq(node=node))
with self.assertRaises(DiscoInfoMalformed):
hash_ = compute_caps_hash(info)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_error_parsing.py 0000664 0000000 0000000 00000002603 14130341156 0024064 0 ustar 00root root 0000000 0000000 import unittest
from nbxmpp.protocol import Iq
from nbxmpp.protocol import JID
from nbxmpp.util import error_factory
class TestErrorParsing(unittest.TestCase):
def test_error_parsing(self):
stanza = '''
File too large. The maximum file size is 20000 bytesFile zu groß. Erlaubt sind 20000 bytes20000'''
error = error_factory(Iq(node=stanza))
self.assertEqual(error.condition, 'not-acceptable')
self.assertEqual(error.app_condition, 'file-too-large')
self.assertEqual(error.get_text(), 'File too large. The maximum file size is 20000 bytes')
self.assertEqual(error.get_text('de'), 'File zu groß. Erlaubt sind 20000 bytes')
self.assertEqual(error.type, 'modify')
self.assertEqual(error.id, 'step_03')
self.assertEqual(error.jid, JID.from_string('upload.montague.tld'))
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_jid_parsing.py 0000664 0000000 0000000 00000013326 14130341156 0023505 0 ustar 00root root 0000000 0000000 import unittest
from nbxmpp.protocol import LocalpartByteLimit
from nbxmpp.protocol import LocalpartNotAllowedChar
from nbxmpp.protocol import ResourcepartByteLimit
from nbxmpp.protocol import ResourcepartNotAllowedChar
from nbxmpp.protocol import DomainpartByteLimit
from nbxmpp.protocol import DomainpartNotAllowedChar
from nbxmpp.protocol import JID
class JIDParsing(unittest.TestCase):
def test_valid_jids(self):
tests = [
'juliet@example.com',
'juliet@example.com/foo',
'juliet@example.com/foo bar',
'juliet@example.com/foo@bar',
'foo\\20bar@example.com',
'fussball@example.com',
'fu\U000000DFball@example.com',
'\U000003C0@example.com',
'\U000003A3@example.com/foo',
'\U000003C3@example.com/foo',
'\U000003C2@example.com/foo',
'king@example.com/\U0000265A',
'example.com',
'example.com/foobar',
'a.example.com/b@example.net',
]
for jid in tests:
JID.from_string(jid)
def test_invalid_jids(self):
tests = [
('"juliet"@example.com', LocalpartNotAllowedChar),
('foo bar@example.com', LocalpartNotAllowedChar),
('henry\U00002163@example.com', LocalpartNotAllowedChar),
('@example.com', LocalpartByteLimit),
('user@example.com/', ResourcepartByteLimit),
('user@example.com/\U00000001', ResourcepartNotAllowedChar),
('\U0000265A@example.com', LocalpartNotAllowedChar),
('user@host@example.com', DomainpartNotAllowedChar),
('juliet@', DomainpartByteLimit),
('/foobar', DomainpartByteLimit),
]
for jid, exception in tests:
with self.assertRaises(exception):
JID.from_string(jid)
def test_ip_literals(self):
tests = [
('juliet@[2002:4559:1FE2::4559:1FE2]/res'),
('juliet@123.123.123.123/res'),
]
for jid in tests:
JID.from_string(jid)
def test_jid_equality(self):
tests = [
'juliet@example.com',
'juliet@example.com/foo',
'example.com',
]
for jid in tests:
self.assertTrue(JID.from_string(jid) == JID.from_string(jid))
def test_jid_escaping(self):
# (user input, escaped)
tests = [
(r'space cadet@example.com',
r'space\20cadet@example.com'),
(r'call me "ishmael"@example.com',
r'call\20me\20\22ishmael\22@example.com'),
(r'at&t guy@example.com',
r'at\26t\20guy@example.com'),
('d\'artagnan@example.com',
r'd\27artagnan@example.com'),
(r'/.fanboy@example.com',
r'\2f.fanboy@example.com'),
(r'::foo::@example.com',
r'\3a\3afoo\3a\3a@example.com'),
(r'@example.com',
r'\3cfoo\3e@example.com'),
(r'user@host@example.com',
r'user\40host@example.com'),
(r'c:\net@example.com',
r'c\3a\net@example.com'),
(r'c:\\net@example.com',
r'c\3a\\net@example.com'),
(r'c:\cool stuff@example.com',
r'c\3a\cool\20stuff@example.com'),
(r'c:\5commas@example.com',
r'c\3a\5c5commas@example.com'),
(r'call me\20@example.com',
r'call\20me\5c20@example.com'),
]
tests2 = [
'juliet@example.com',
'juliet@example.com',
'juliet@example.com',
'juliet@example.com',
'fussball@example.com',
'fu\U000000DFball@example.com',
'\U000003C0@example.com',
'\U000003A3@example.com',
'\U000003C3@example.com',
'\U000003C2@example.com',
'example.com',
]
test3 = [
'\\20callme\\20@example.com',
'\\20callme@example.com',
'callme\\20@example.com',
]
test4 = [
('call\\20me@example.com', 'call me@example.com',)
]
fail_tests = [
r'c:\5commas@example.com/asd',
r'juliet@example.com/test'
]
for user_input, escaped in tests:
# Parse user input and escape it
jid = JID.from_user_input(user_input)
self.assertTrue(jid.domain == 'example.com')
self.assertTrue(str(jid) == escaped)
self.assertTrue(jid.to_user_string() == user_input)
# We must fail on invalid JIDs
with self.assertRaises(Exception):
JID.from_string(user_input)
# Parse escaped JIDs
jid = JID.from_string(escaped)
self.assertTrue(str(jid) == escaped)
self.assertTrue(jid.domain == 'example.com')
for jid in tests2:
# show that from_string() and from_user_input() produce the same
# result for valid bare JIDs
self.assertTrue(JID.from_string(jid) == JID.from_user_input(jid))
for jid in test3:
# JIDs starting or ending with \20 are not escaped
self.assertTrue(JID.from_string(jid).to_user_string() == jid)
for user_input, user_string in test4:
# Test escaped keyword argument
self.assertTrue(JID.from_user_input(user_input, escaped=True) != JID.from_user_input(user_input))
self.assertTrue(JID.from_user_input(user_input, escaped=True).to_user_string() == user_string)
for user_input in fail_tests:
# from_user_input does only support bare jids
with self.assertRaises(Exception):
JID.from_user_input(user_input)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_location.py 0000664 0000000 0000000 00000010410 14130341156 0023013 0 ustar 00root root 0000000 0000000 from test.lib.util import StanzaHandlerTest
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import LocationData
from nbxmpp.structs import PubSubEventData
class LocationTest(StanzaHandlerTest):
def test_location_parsing(self):
def _on_message(_con, _stanza, properties):
data = LocationData(accuracy='20',
alt='1609',
altaccuracy='10',
area='Central Park',
bearing='12.33',
building='The Empire State Building',
country='United States',
countrycode='US',
datum='Some datum',
description='Bill\'s house',
error='290.8882087',
floor='102',
lat='39.75',
locality='New York City',
lon='-104.99',
postalcode='10118',
region='New York',
room='Observatory',
speed='52.69',
street='350 Fifth Avenue / 34th and Broadway',
text='Northwest corner of the lobby',
timestamp='2004-02-19T21:12Z',
tzo='-07:00',
uri='http://www.nyc.com/')
pubsub_event = PubSubEventData(
node='http://jabber.org/protocol/geoloc',
id='d81a52b8-0f9c-11dc-9bc8-001143d5d5db',
item=None,
data=data,
deleted=False,
retracted=False,
purged=False)
# We cant compare Node objects
pubsub_event_ = properties.pubsub_event._replace(item=None)
self.assertEqual(pubsub_event, pubsub_event_)
event = '''
20160910
Central Park
12.33The Empire State BuildingUnited StatesUSSome datumBill's house290.888208710239.75New York City-104.9910118New YorkObservatory52.69350 Fifth Avenue / 34th and BroadwayNorthwest corner of the lobby2004-02-19T21:12Z-07:00http://www.nyc.com/
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT))
self.dispatcher.process_data(event)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_mood.py 0000664 0000000 0000000 00000003274 14130341156 0022153 0 ustar 00root root 0000000 0000000 from test.lib.util import StanzaHandlerTest
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import MoodData
from nbxmpp.structs import PubSubEventData
class MoodTest(StanzaHandlerTest):
def test_mood_parsing(self):
def _on_message(_con, _stanza, properties):
data = MoodData(mood='annoyed', text='curse my nurse!')
pubsub_event = PubSubEventData(
node='http://jabber.org/protocol/mood',
id='a475804a-0f9c-11dc-98a8-001143d5d5db',
item=None,
data=data,
deleted=False,
retracted=False,
purged=False)
# We cant compare Node objects
pubsub_event_ = properties.pubsub_event._replace(item=None)
self.assertEqual(pubsub_event, pubsub_event_)
event = '''
curse my nurse!
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT))
self.dispatcher.process_data(event)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_pubsub.py 0000664 0000000 0000000 00000006127 14130341156 0022515 0 ustar 00root root 0000000 0000000 from test.lib.util import StanzaHandlerTest
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import PubSubEventData
class PubsubTest(StanzaHandlerTest):
def test_purge_event(self):
def _on_message(_con, _stanza, properties):
pubsub_event = PubSubEventData(
node='princely_musings',
id=None,
item=None,
data=None,
deleted=False,
retracted=False,
purged=True)
self.assertEqual(pubsub_event, properties.pubsub_event)
event = '''
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT,
priority=16))
self.dispatcher.process_data(event)
def test_delete_event(self):
def _on_message(_con, _stanza, properties):
pubsub_event = PubSubEventData(
node='princely_musings',
id=None,
item=None,
data=None,
deleted=True,
retracted=False,
purged=False)
self.assertEqual(pubsub_event, properties.pubsub_event)
event = '''
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT,
priority=16))
self.dispatcher.process_data(event)
def test_retracted_event(self):
def _on_message(_con, _stanza, properties):
pubsub_event = PubSubEventData(
node='princely_musings',
id='ae890ac52d0df67ed7cfdf51b644e901',
item=None,
data=None,
deleted=False,
retracted=True,
purged=False)
self.assertEqual(pubsub_event, properties.pubsub_event)
event = '''
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT,
priority=16))
self.dispatcher.process_data(event)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_sasl_scram.py 0000664 0000000 0000000 00000002547 14130341156 0023346 0 ustar 00root root 0000000 0000000 import unittest
from unittest.mock import Mock
from nbxmpp.auth import SCRAM_SHA_1
from nbxmpp.util import b64encode
# Test vector from https://wiki.xmpp.org/web/SASL_and_SCRAM-SHA-1
class SCRAM(unittest.TestCase):
def setUp(self):
self.con = Mock()
self._method = SCRAM_SHA_1(self.con, None)
self._method._client_nonce = 'fyko+d2lbbFgONRv9qkxdawL'
self.maxDiff = None
self._username = 'user'
self._password = 'pencil'
self.auth = '%s' % b64encode('n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL')
self.challenge = b64encode('r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096')
self.response = '%s' % b64encode('c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=')
self.success = b64encode('v=rmF9pqV8S7suAoZWja4dJRkFsKQ=')
def test_auth(self):
self._method.initiate(self._username, self._password)
self.assertEqual(self.auth, str(self.con.send_nonza.call_args[0][0]))
self._method.response(self.challenge)
self.assertEqual(self.response, str(self.con.send_nonza.call_args[0][0]))
self._method.success(self.success)
if __name__ == '__main__':
unittest.main()
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_tune.py 0000664 0000000 0000000 00000004347 14130341156 0022172 0 ustar 00root root 0000000 0000000 from test.lib.util import StanzaHandlerTest
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import TuneData
from nbxmpp.structs import PubSubEventData
class TuneTest(StanzaHandlerTest):
def test_tune_parsing(self):
def _on_message(_con, _stanza, properties):
data = TuneData(artist='Yes',
length='686',
rating='8',
source='Yessongs',
title='Heart of the Sunrise',
track='3',
uri='https://www.artist.com')
pubsub_event = PubSubEventData(
node='http://jabber.org/protocol/tune',
id='bffe6584-0f9c-11dc-84ba-001143d5d5db',
item=None,
data=data,
deleted=False,
retracted=False,
purged=False)
# We cant compare Node objects
pubsub_event_ = properties.pubsub_event._replace(item=None)
self.assertEqual(pubsub_event, pubsub_event_)
event = '''
Yes6868YessongsHeart of the Sunrisehttps://www.artist.com
'''
self.dispatcher.register_handler(
StanzaHandler(name='message',
callback=_on_message,
ns=Namespace.PUBSUB_EVENT))
self.dispatcher.process_data(event)
python-nbxmpp-nbxmpp-2.0.4/test/unit/test_xml_vulnerability.py 0000664 0000000 0000000 00000004525 14130341156 0024766 0 ustar 00root root 0000000 0000000 import unittest
from unittest.mock import Mock
from nbxmpp import dispatcher
class XMLVulnerability(unittest.TestCase):
def setUp(self):
self.stream = Mock()
self.stream.is_websocket = False
self.dispatcher = dispatcher.StanzaDispatcher(self.stream)
self._error_handler = Mock()
self.dispatcher.subscribe('parsing-error', self._error_handler)
self.dispatcher.reset_parser()
def test_exponential_entity_expansion(self):
bomb = """
]>
&c;"""
self.dispatcher.process_data(bomb)
self._error_handler.assert_called()
def test_quadratic_blowup(self):
bomb = """
]>
&a;&a;&a;... repeat"""
self.dispatcher.process_data(bomb)
self._error_handler.assert_called()
def test_external_entity_expansion(self):
bomb = """
]>
ⅇ"""
self.dispatcher.process_data(bomb)
self._error_handler.assert_called()
def test_external_local_entity_expansion(self):
bomb = """
]>
ⅇ"""
self.dispatcher.process_data(bomb)
self._error_handler.assert_called()
def test_dtd_retrival(self):
bomb = """
text
"""
self.dispatcher.process_data(bomb)
self._error_handler.assert_called()
if __name__ == '__main__':
unittest.main()