pax_global_header00006660000000000000000000000064152436151330014515gustar00rootroot0000000000000052 comment=51b4aee436b12a280b7103ffb0378382912b0533 python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/000077500000000000000000000000001524361513300214545ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/.chglog/000077500000000000000000000000001524361513300227755ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/.chglog/CHANGELOG.tpl.md000077500000000000000000000006731524361513300254150ustar00rootroot00000000000000{{ range .Versions }} nbxmpp {{ .Tag.Name }} ({{ datetime "02 Jan 2006" .Tag.Date }}) {{ range .CommitGroups -}} {{ if .Title }} {{ .Title }}{{end}} {{ range .Commits -}} {{ if .Subject }} * {{ .Subject }}{{end}}{{if .Refs}} ({{range .Refs}}#{{.Ref}}{{end}}){{end}} {{ end }} {{ end -}} {{- if .NoteGroups -}} {{ range .NoteGroups -}} {{ .Title }} {{ range .Notes }} * {{ .Body }} {{ end }} {{ end -}} {{ end -}} {{break}} {{ end -}} python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/.chglog/config.yml000077500000000000000000000012621524361513300247710ustar00rootroot00000000000000style: gitlab template: CHANGELOG.tpl.md info: title: CHANGELOG repository_url: https://dev.gajim.org/gajim/python-nbxmpp options: commits: filters: Type: - feat - imprv - change - perf - fix commit_groups: sort_by: Custom title_order: - feat - imprv - change - perf - fix title_maps: feat: New imprv: Improvements change: Change perf: Performance fix: Bug Fixes header: pattern: "^(\\w*)\\:\\s(.*)$" pattern_maps: - Type - Subject issues: prefix: - "#" refs: actions: - Fixes notes: keywords: - Notes python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/.gitignore000066400000000000000000000002021524361513300234360ustar00rootroot00000000000000build/ __pycache__/ .mypy_cache/ .venv/ nbxmpp.egg-info debian_build/ dist .idea *~ *.sublime-workspace *.sublime-project uv.lock python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/.gitlab-ci.yml000066400000000000000000000043101524361513300241060ustar00rootroot00000000000000image: nbxmpp-master:latest stages: - test - build - deploy - publish test-isort: stage: test rules: - when: always script: - isort --version - isort --check . interruptible: true test-black: stage: test rules: - when: always script: - black --version - black --check . interruptible: true test-ruff: stage: test rules: - when: always script: - ruff --version - ruff check . interruptible: true test-pyright: stage: test rules: - when: always script: - pip install pygobject-stubs==2.16.0 - pyright --version - pyright interruptible: true test-unittest: stage: test rules: - when: always script: - python -m unittest discover -v interruptible: true build-unix: stage: build dependencies: [] rules: - when: always script: - pip install git+https://dev.gajim.org/gajim/release-helper.git - pip install build - python3 -m build - release-helper build-debian-pkg "$(find dist/nbxmpp-*.tar.gz)" 1 --pkgprefix=python3 --pkgsuffix=nightly artifacts: name: "nbxmpp-$CI_COMMIT_SHA" expire_in: 1 week paths: - dist/nbxmpp-*.tar.gz - dist/nbxmpp-*.whl - debian_build/* deploy-pypi: image: nbxmpp-deploy:latest stage: deploy dependencies: - "build-unix" rules: - if: '$CI_COMMIT_TAG' script: - twine upload --username=__token__ --password=$PYPI_TOKEN dist/* deploy-debian-nightly: image: nbxmpp-deploy:latest stage: deploy dependencies: - build-unix rules: - if: '$NIGHTLY_BUILD' allow_failure: exit_codes: - 100 script: - | if [ "$FORCE_DEB_DEPLOY" != "true" ] then release-helper nightly-check fi - > release-helper deploy-to-ftp \ --host=$FTP_HOST \ --user=$FTP_USER \ --password=$FTP_PASS \ --directory=debian/nbxmpp/"$(date +'%Y%m%d')" \ debian_build publish-release: image: nbxmpp-publish:latest stage: publish dependencies: [] rules: - if: '$CI_COMMIT_TAG' script: - > release-helper create-release \ $CI_PROJECT_ID \ $CI_RELEASE_TOKEN \ --version=$CI_COMMIT_TAG \ --tag=$CI_COMMIT_TAG python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/.pre-commit-config.yaml000066400000000000000000000007721524361513300257430ustar00rootroot00000000000000repos: - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.14.8 hooks: - id: ruff exclude: ".githooks/" - repo: https://github.com/pycqa/isort rev: 7.0.0 hooks: - id: isort - repo: https://github.com/psf/black # The `refs/tags/:refs/tags/` is needed for black's required-version to work: # https://github.com/psf/black/issues/2493#issuecomment-1081987650 rev: 'refs/tags/24.10.0:refs/tags/24.10.0' hooks: - id: black python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/COPYING000066400000000000000000001045131524361513300225130ustar00rootroot00000000000000 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/ChangeLog000066400000000000000000000337411524361513300232360ustar00rootroot00000000000000nbxmpp 7.4.0 (26 Aug 2026) New * Client: Add better disconnect check Improvements * VCardAvatars: Validate and normalize sha1 * Fallback: Improve range validation * Use secrets module instead of random * OMEMO: Harden bundle parser * OMEMO: Harden devicelist parser Bug Fixes * WebSocket: Disconnect on parsing errors * VCardTemp: Guard against base64 decoding errors * Client: Add timeout when waiting for stream end * MDS: Add request_items dependency nbxmpp 7.3.0 (30 Jun 2026) New * Add helper method parse_account_invite() Improvements * OpenPGP: Don't add fallback body text Bug Fixes * EME: Make name always a string nbxmpp 7.2.0 (19 Apr 2026) New * Implement XEP-445 Pre-Auth IBR Improvements * OpenPGP: Make sure fingerprints are always upper case * VCard4: Request access model before publishing * Presence: Add preauth attribute * Protocol: Add feature method for pre approval nbxmpp 7.1.0 (22 Feb 2026) Improvements * SASL: Improve logging for GSSAPI errors * Fallback: Be more strict in accepting ranges * Add Node.topretty() Change * OGP: Accept only data uris as image nbxmpp 7.0.0 (13 Dec 2025) Change * Remove HTTP Requests API * Client: Add set_host_meta_data() nbxmpp 6.4.0 (01 Nov 2025) New * Pubsub: Add subscribe/unsubscribe methods * VCard4: Parse notification events * VCard4: Add get_parameter() method for easier access * Support for open graph data embedded in message * Add support for reaction restrictions Improvements * Add pronouns field to VCard4 * Support for hats "hue" attribute Bug Fixes * Connection: Allow DNS requests for "direct" proxies nbxmpp 6.3.1 (25 Sep 2025) Improvements * Log details about invalid MUC affiliation items * Log details about invalid roster items Bug Fixes * Leave text condition in place in CommonError.get_text() * Do not treat thread subject as MUC subject * Websocket: Disconnect signals on finalize nbxmpp 6.3.0 (07 Aug 2025) New * Allow to set stream language Improvements * Better cache invalid xml regex * HTTP: Log warning on cancel instead of raising exception Bug Fixes * SASL: Apply SASLPrep for PLAIN and SCRAM * VCard4: Preserve unsupported elements nbxmpp 6.2.0 (19 Apr 2025) Change * Retraction: Rewrite parsing code Bug Fixes * Dataforms: Fix init list fields with no options (#168) nbxmpp 6.1.1 (13 Apr 2025) Improvements * MUC: Return message id after sending Bug Fixes * VoiceRequests: Don’t parse errors * MDS: Make by attribute configurable nbxmpp 6.1.0 (01 Apr 2025) New * MDS: Allow setting MDS assist on message Improvements * SASL: Improve logging Change * MDS: Simplify set_mds() nbxmpp 6.0.2 (11 Mar 2025) Improvements * Retraction: Parse id attribute for tombstones Bug Fixes * OpenPGP: Depreacted pubkey date attribute nbxmpp 6.0.1 (03 Mar 2025) Improvements * DiscoInfo: Add more MUC config fields Bug Fixes * DiscoInfo: Return correct values in get_field_value() * JID: Raise error on IRIs with auth component nbxmpp 6.0.0 (24 Feb 2025) Change * JID: Remove default arguments Bug Fixes * Moderation: Parse moderation messages correctly (#164) nbxmpp 5.0.4 (19 Sep 2024) Bug Fixes * Bookmarks: Store password * Hats: Make struct compareable * Pubsub: Don’t process pubsub events from full jids (#159) nbxmpp 5.0.3 (23 Jul 2024) Bug Fixes * Delay: Consider MUC and own server (#156) nbxmpp 5.0.2 (07 Jul 2024) Bug Fixes * Delay: Better discover server delays (#156) nbxmpp 5.0.1 (20 Jun 2024) Improvements * SecurityLabels: Add restrict attribute nbxmpp 5.0.0 (04 Jun 2024) New * Update XEP-0425 to latest version * Parse XEP-0172 nickname in MUC presences * Add support for multi language bodies and fallbacks * XEP-0490: Message Displayed Synchronization * XEP-0317 (Hats) * MUC: Process muc user data for all message types * XEP-0424 (Message Retraction) * Add Jingle Feedback namespace * Add XEP-0215 namespace Improvements * Protocol: Improve error message when parsing JIDs * Add new moderation namespace disco info property * Improve support for full stanza encryption * Add EncryptionData struct * Replies: Return JID instead of string * Protocol: Remove deprecated 'code' attribute usage in Errors Bug Fixes * Message: Fix false positive muc pm detection * IBB: Fix parsing open stanza nbxmpp 4.5.4 (12 Feb 2024) New * Add XEP-0353 namespace Bug Fixes * Correctly discover subject change (#154) * DateTime: Be more strict with parsing nbxmpp 4.5.3 (21 Nov 2023) New * Websocket: Make TLS properties available Bug Fixes * SASL2: Make login test mode work * Websocket: Set user agent (#132) nbxmpp 4.5.2 (06 Nov 2023) Bug Fixes * PubSub: Narrow accepted message types nbxmpp 4.5.1 (06 Nov 2023) Bug Fixes * SASL: Add back lost ANONYMOUS auth mechanism nbxmpp 4.5.0 (24 Oct 2023) New * Support XEP-0440 SASL Channel-Binding Type Capability Change * Raise GLib version nbxmpp 4.4.0 (22 Oct 2023) New * Add DiscoInfo.has_identity() * JID: Add JID.from_iri() Improvements * JID: Improve compliance when generating IRIs Change * Chatstates: Accept chatstates with delay tag nbxmpp 4.3.3 (28 Aug 2023) Improvements * JID: Allow comparisons against any object * DiscoInfo: Improve discovery if subject is allowed to be modified Bug Fixes * Don’t expect localpart for bare JIDs nbxmpp 4.3.2 (04 Jul 2023) New * SecurityLabels: Add get_label_hash() * JID: Add sqlite3 adapter magic method * MessageProperties: Add origin_id attribute * MessageProperties: Add remote_jid attribute Improvements * Client: Return only copy of ignored errors * CommonError: Add 'by' attribute * Moderation: Parse more data Bug Fixes * HTTP: Add set_accept_certificate() nbxmpp 4.3.1 (28 May 2023) Bug Fixes * HTTP: Abort correctly on content overflow * Websocket: Always set peer certificate nbxmpp 4.3.0 (21 May 2023) New * Add option to force http1 * Add method to generate XMPP IRIs Improvements * Lower log level for missing GSSAPI dependency Change * OMEMO: Add device id and namespace to OMEMOBundle Bug Fixes * Don’t delete session object in cleanup() * HTTP: Don’t accept content encoding (#143) nbxmpp 4.2.2 (25 Mar 2023) Bug Fixes * HTTP: Reset attributes on redirect (#141) nbxmpp 4.2.1 (18 Mar 2023) Bug Fixes * HTTP: Make sure streams are closed only once (#139) nbxmpp 4.2.0 (05 Feb 2023) New * Add support for XEP-0461: Message Replies Improvements * Add SASLprep profile Change * Require PyGObject 3.42.0 nbxmpp 4.1.0 (24 Jan 2023) Change * Port package to pyproject.toml Bug Fixes * Handle race condition when cancelling request (#136) nbxmpp 4.0.1 (10 Jan 2023) Bug Fixes * HTTP: Parse uri with correct flags nbxmpp 4.0.0 (06 Jan 2023) New * Add message reactions (XEP-0444) support * Add HTTP module Change * Raise required python version to 3.10 * Remove example client * Port to libsoup3 * XEP-0392: Update to 0.8.0 nbxmpp 3.2.5 (26 Oct 2022) New * Add support for XEP-0421 Bug Fixes * MUC: Check correctly for muc subject * Client: Don’t override modified certification errors nbxmpp 3.2.4 (05 Oct 2022) Improvements * Tasks: Track timeouts within the task Bug Fixes * MUC: Relax definition of MUC config change nbxmpp 3.2.3 (28 Sep 2022) Bug Fixes * TCP: Improve detection of broken connection nbxmpp 3.2.2 (13 Sep 2022) New * Expose TLS version and ciphersuite * Add stringprep implementation Change * Add packaging as dependency * Use stringprep instead of precis for JID validation Bug Fixes * Add missing __init__.py to examples Notes * You can set the env variable NBXMPP_USE_PRECIS to revert back to the old behavior. nbxmpp 3.2.1 (29 Aug 2022) Bug Fixes * Prepare domain names with UTS46 nbxmpp 3.2.0 (28 Aug 2022) New * Add support for Extensible SASL Profile (XEP-0388) Bug Fixes * Set timeout for host-meta http query (#130) nbxmpp 3.1.1 (27 Jul 2022) Improvements * Add more datetime test cases Performance * Use cache when calling JID.new_as_bare() Bug Fixes * Allow handlers to connect to presence type available nbxmpp 3.1.0 (28 May 2022) New * Examples: Allow to connect with different modes Bug Fixes * Client: Disconnect after stream restart in Login mode nbxmpp 3.0.2 (18 May 2022) Bug Fixes * Roster: Raise exception if a full jid is found * AdHoc: Don’t raise on empty nodes * Roster: Don’t fail when no roster attribute is found * #126 Remove redundant wheel dependency nbxmpp 3.0.1 (09 May 2022) Bugfix * Fix non-installable release nbxmpp 3.0.0 (09 May 2022) New * Add Roster module * Add Last Activity module * Add EntityTime module * Bookmarks: Add extensions support * Add Message Moderation * Implement SCRAM-SHA-512 Bugfixes * AdHoc: Improve XEP-0050 Compliance * Correctly handle RSM count request * Correction: Ignore messages where message id == replace id * Blocking: Don’t abort processing after parsing unblocks * Handle multiple stanza-id elements Others * Many breaking changes to methods and structs nbxmpp 2.0.6 (14 March 2022) Bugfixes * Don’t fail on invalid host meta file nbxmpp 2.0.5 (13 March 2022) Bugfixes * Fix bookmarks compat namespace * Resolve websocket uri only via https nbxmpp 2.0.4 (09 October 2021) Bugfixes * Ignore messages with incorrect id * AdHoc: Make parsing AdHoc commands more compliant nbxmpp 2.0.3 (24 July 2021) Bugfixes * Correctly handle RSM count request * Improve XEP-0050 Compliance * GSSAPI: Catch OSError 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 nbxmpp 2.0.1 (07 February 2021) New * Properties: is_from_us() Method Bugfixes * Fix handling invalid websocket uris 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 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 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 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 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 nbxmpp 0.6.9 (10 January 2019) * Always bind after SM failed Fixes #64 * Dont try and guess system language nbxmpp 0.6.8 (07 October 2018) * Reset SM counter after receiving * Issue event when SM resume fails 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 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 nbxmpp 0.6.5 (30 April 2018) * Fix BOSH usage (don't set ALPN) * Better handling of certificate files nbxmpp 0.6.4 (17 March 2018) * Fix SOCKS5 usage nbxmpp 0.6.3 (26 January 2018) * Add ALPN and SNI support for when using DirectTLS * Bugfixes nbxmpp 0.6.2 (27 December 2017) * Correctly load client certs * Warn on any error in the certificate chain * Fixed a traceback loop nbxmpp 0.6.1 (29 November 2017) * Add new getStanzaIDAttrs method * Fix BOSH connexion * stop using PyOpenSSL for random generator 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 nbxmpp 0.5.6 (03 June 2017) * Support XEP-0198 Version 1.5.2 * Add new XEP Namespaces nbxmpp 0.5.5 (30 January 2017) * Some cleanup * Add some namespaces 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 nbxmpp 0.5.3 (13 July 2015) * Fix receiving long utf8 strings under py3 * Fix issue with pyopenssl 0.15.1 * Fix decoding issues nbxmpp 0.5.2 (27 December 2014) * Fix BOSH HTTP requests * Fix handling of binary SASL data for mechanism GSSAPI * Update MAM namespace nbxmpp 0.5.1 (04 October 2014) * Fix printing network errors in a non-utf-8 console nbxmpp 0.5 (02 July 2014) * support both python2 and python3 * Fix storing server certificate when there is no error nbxmpp 0.4 (15 March 2014) * Ability to configure TLS protocol version * Add support for SCRAM-SHA-1-PLUS * Security improvements nbxmpp 0.3 (23 December 2013) * Improve security level * Ability to configure cipher list * Store only depth 0 SSL certificate nbxmpp 0.2 (26 July 2013) * Add some namespace * do TLS handshake without blocking * store all SSL errors instead of only last one nbxmpp 0.1 (05 August 2012) * Initial release python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/LICENSES/000077500000000000000000000000001524361513300226615ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/LICENSES/GPL-2.0-or-later.txt000066400000000000000000000416711524361513300260750ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. 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 convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/LICENSES/GPL-3.0-or-later.txt000066400000000000000000001035621524361513300260740ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/LICENSES/MIT.txt000066400000000000000000000020661524361513300240570ustar00rootroot00000000000000MIT License Copyright (c) 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. python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/MANIFEST.in000066400000000000000000000001331524361513300232070ustar00rootroot00000000000000include ChangeLog COPYING README recursive-include nbxmpp *.py recursive-include test *.py python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/README.md000066400000000000000000000022761524361513300227420ustar00rootroot00000000000000# 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](https://www.python.org/) (>=3.10) - [PyGObject](https://pypi.org/project/PyGObject/) (>=3.42.0) - [GLib](https://gitlab.com/gnome/glib) (>=2.66.0) - [libsoup3](https://libsoup.gnome.org/) - [precis-i18n](https://pypi.org/project/precis-i18n/) (>=1.0.0) - [packaging](https://pypi.org/project/packaging/) - [idna](https://pypi.org/project/idna/) ## Optional Runtime Requirements - [python-gssapi](https://pypi.org/project/gssapi/) GSSAPI authentication ## Build Requirements - [setuptools](https://pypi.org/project/setuptools/) (>=65.0.0) ## Tests - `python -m unittest discover -s test` ## Features - List of [supported XEPs](https://xmpp.org/software/libraries/python-nbxmpp/) ### Setup Run the following: ```shell pip install . ``` or ```shell pip install .[gssapi] ``` to also install the optional dependency `gssapi`. ### Usage To use python-nbxmpp, `import nbxmpp` in your application. To see an example check out [nbxmpp-client](https://pypi.org/project/nbxmpp-client/) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/debian/000077500000000000000000000000001524361513300226765ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/debian/changelog000066400000000000000000000002411524361513300245450ustar00rootroot00000000000000python3-nbxmpp-nightly ({DATE}) unstable; urgency=low * https://dev.gajim.org/gajim/python-nbxmpp/-/commits/master -- nbxmpp CI {DATE_TIME} python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/debian/control000066400000000000000000000017421524361513300243050ustar00rootroot00000000000000Source: python3-nbxmpp-nightly Section: python Priority: optional Maintainer: Gajim Maintainers Build-Depends: debhelper-compat (= 13), dh-python, gir1.2-soup-3.0, python3-all, python3-gi (>=3.42.0), python3-idna, python3-packaging, python3-precis-i18n (>=1.0.0), python3-setuptools (>= 65.0.0), Standards-Version: 4.1.4 Rules-Requires-Root: no Homepage: https://dev.gajim.org/gajim/python-nbxmpp Vcs-Git: https://dev.gajim.org/gajim/python-nbxmpp.git Vcs-Browser: https://dev.gajim.org/gajim/python-nbxmpp/tree/master Package: python3-nbxmpp-nightly Architecture: all Depends: ${misc:Depends}, ${python3:Depends}, gir1.2-soup-3.0, python3-gi (>=3.42.0), Recommends: python3-gssapi, Breaks: gajim (<< 1.6~) Conflicts: python3-nbxmpp Description: Non blocking XMPP Python 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. python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/debian/copyright000066400000000000000000000013651524361513300246360ustar00rootroot00000000000000Upstream Authors: - Yann Leboulanger - Philipp Hörist Copyright: (c) 2022 Gajim Team 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 . python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/debian/rules000077500000000000000000000002361524361513300237570ustar00rootroot00000000000000#!/usr/bin/make -f DPKG_EXPORT_BUILDFLAGS = 1 include /usr/share/dpkg/default.mk export PYBUILD_NAME=nbxmpp %: dh $@ --with python3 --buildsystem=pybuild python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/debian/source/000077500000000000000000000000001524361513300241765ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/debian/source/format000066400000000000000000000000141524361513300254040ustar00rootroot000000000000003.0 (quilt) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/000077500000000000000000000000001524361513300227605ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/__init__.py000066400000000000000000000001661524361513300250740ustar00rootroot00000000000000import gi gi.require_version("Soup", "3.0") from .protocol import * # noqa: F403, E402 __version__: str = "7.4.0" python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/addresses.py000066400000000000000000000153341524361513300253150ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later import logging from collections.abc import Iterator from nbxmpp.const import ConnectionProtocol from nbxmpp.const import ConnectionType from nbxmpp.structs import ProxyData from nbxmpp.structs import ServerAddress from nbxmpp.types import CustomHostT from nbxmpp.util import Observable from nbxmpp.util import parse_websocket_uri log = logging.getLogger("nbxmpp.addresses") class ServerAddresses(Observable): """ Signals: resolved """ def __init__(self, domain: str | None) -> None: Observable.__init__(self, log) self._domain = domain self._host_meta_data: bytes | None = None self._custom_host: CustomHostT | None = None self._proxy: ProxyData | None = None self._is_resolved = False self._addresses: list[ServerAddress] = [ 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: list[ServerAddress] = [ 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) -> str | None: return self._domain @property def is_resolved(self) -> bool: return self._is_resolved def set_host_meta_data(self, data: bytes | None) -> None: self._host_meta_data = data def resolve(self) -> None: 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 self._resolve_alternatives() def _resolve_alternatives(self) -> None: if not self._host_meta_data: log.info("No host meta data to process") self._on_request_resolved() return try: uri = parse_websocket_uri(self._host_meta_data.decode()) except Exception as error: log.info("Error parsing websocket uri: %s", error) 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 cancel_resolve(self) -> None: self.remove_subscriptions() def set_custom_host(self, address: CustomHostT | None) -> None: # 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: ProxyData | None) -> None: self._proxy = proxy def _on_request_resolved(self) -> None: self._is_resolved = True self.notify("resolved") self.remove_subscriptions() def get_next_address( self, allowed_types: list[ConnectionType], allowed_protocols: list[ConnectionProtocol], ) -> Iterator[ServerAddress]: """ 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: ServerAddress) -> ServerAddress: 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: list[ServerAddress], allowed_types: list[ConnectionType], allowed_protocols: list[ConnectionProtocol], ) -> Iterator[ServerAddress]: if self._proxy is not None and self._proxy.type != "direct": 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) -> str: addresses = self._addresses + self._fallback_addresses return "\n".join([str(addr) for addr in addresses]) class NoMoreAddresses(Exception): pass python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/client.py000066400000000000000000001220601524361513300246110ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import Literal from typing import overload from typing import TYPE_CHECKING import logging from collections.abc import Callable from collections.abc import Iterator from collections.abc import Sequence from gi.repository import Gio from gi.repository import GLib from nbxmpp.addresses import NoMoreAddresses from nbxmpp.addresses import ServerAddress from nbxmpp.addresses import ServerAddresses from nbxmpp.const import ConnectionProtocol from nbxmpp.const import ConnectionType from nbxmpp.const import Mode from nbxmpp.const import StreamError from nbxmpp.const import StreamState from nbxmpp.dispatcher import StanzaDispatcher from nbxmpp.namespaces import Namespace from nbxmpp.protocol import BindRequest from nbxmpp.protocol import Features from nbxmpp.protocol import isResultNode from nbxmpp.protocol import JID from nbxmpp.protocol import Protocol from nbxmpp.protocol import SessionRequest from nbxmpp.protocol import StanzaMalformed from nbxmpp.protocol import TLSRequest from nbxmpp.protocol import WebsocketCloseHeader from nbxmpp.sasl import SASL from nbxmpp.simplexml import Node from nbxmpp.smacks import Smacks from nbxmpp.structs import ProxyData from nbxmpp.task import Task from nbxmpp.tcp import TCPConnection from nbxmpp.types import CustomHostT from nbxmpp.util import generate_id from nbxmpp.util import get_stream_header from nbxmpp.util import LogAdapter from nbxmpp.util import Observable from nbxmpp.util import validate_stream_header from nbxmpp.websocket import WebsocketConnection if TYPE_CHECKING: from nbxmpp.connection import Connection from nbxmpp.dispatcher import NBXMPPModuleNameT from nbxmpp.dispatcher import NBXMPPModuleT from nbxmpp.modules.activity import Activity from nbxmpp.modules.adhoc import AdHoc from nbxmpp.modules.annotations import Annotations from nbxmpp.modules.attention import Attention from nbxmpp.modules.blocking import Blocking from nbxmpp.modules.bookmarks.native_bookmarks import NativeBookmarks from nbxmpp.modules.bookmarks.pep_bookmarks import PEPBookmarks from nbxmpp.modules.bookmarks.private_bookmarks import PrivateBookmarks from nbxmpp.modules.captcha import Captcha from nbxmpp.modules.chat_markers import ChatMarkers from nbxmpp.modules.chatstates import Chatstates from nbxmpp.modules.correction import Correction from nbxmpp.modules.delay import Delay from nbxmpp.modules.delimiter import Delimiter from nbxmpp.modules.discovery import Discovery from nbxmpp.modules.eme import EME from nbxmpp.modules.entity_caps import EntityCaps from nbxmpp.modules.entity_time import EntityTime from nbxmpp.modules.http_auth import HTTPAuth from nbxmpp.modules.http_upload import HTTPUpload from nbxmpp.modules.ibb import IBB from nbxmpp.modules.idle import Idle from nbxmpp.modules.iq import BaseIq from nbxmpp.modules.last_activity import LastActivity from nbxmpp.modules.location import Location from nbxmpp.modules.mam import MAM from nbxmpp.modules.mds import MDS from nbxmpp.modules.message import BaseMessage from nbxmpp.modules.mood import Mood from nbxmpp.modules.muc import MUC from nbxmpp.modules.muc.hats import Hats from nbxmpp.modules.muc.moderation import Moderation from nbxmpp.modules.muclumbus import Muclumbus from nbxmpp.modules.nickname import Nickname from nbxmpp.modules.omemo import OMEMO from nbxmpp.modules.oob import OOB from nbxmpp.modules.openpgp import OpenPGP from nbxmpp.modules.pgplegacy import PGPLegacy from nbxmpp.modules.ping import Ping from nbxmpp.modules.presence import BasePresence from nbxmpp.modules.pubsub import PubSub from nbxmpp.modules.reactions import Reactions from nbxmpp.modules.receipts import Receipts from nbxmpp.modules.register import Register from nbxmpp.modules.replies import Replies from nbxmpp.modules.retraction import Retraction from nbxmpp.modules.roster import Roster from nbxmpp.modules.security_labels import SecurityLabels from nbxmpp.modules.software_version import SoftwareVersion from nbxmpp.modules.tune import Tune from nbxmpp.modules.user_avatar import UserAvatar from nbxmpp.modules.vcard4 import VCard4 from nbxmpp.modules.vcard_avatar import VCardAvatar from nbxmpp.modules.vcard_temp import VCardTemp log = logging.getLogger("nbxmpp.stream") STREAM_END_TIMEOUT_SEC = 2 CONNECTION_CHECK_INTERVAL = 30 CONNECTION_CHECK_TIMEOUT = 3 class Client(Observable): def __init__(self, log_context: str | None = None) -> 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: JID | None = None self._lang: str = "en" self._domain: str | None = None self._username: str | None = None self._resource: str | None = None self._custom_host: CustomHostT | None = None self._addresses: ServerAddresses | None = None self._current_address: ServerAddress | None = None self._address_generator: Iterator[ServerAddress] | None = None self._client_cert: Any = None self._client_cert_pass: str | None = None self._proxy: ProxyData | None = None self._host_meta_data: bytes | None = None self._allowed_con_types: list[ConnectionType] | None = None self._allowed_protocols: list[ConnectionProtocol] | None = None self._allowed_mechs: set[str] | None = None self._sm_disabled = False self._stream_end_timeout_id: int | None = None self._stream_id: str | None = None self._stream_secure = False self._stream_authenticated = False self._stream_features: Features | None = None self._session_required = False self._connect_successful = False self._connection_check_timeout_id: int | None = None self._error: tuple[StreamError | None, str | None, str | None] = ( None, None, None, ) self._ignored_tls_errors: set[Gio.TlsCertificateFlags] = set() self._ignore_tls_errors = False self._accepted_certificates: list[Gio.TlsCertificate] = [] self._peer_certificate: Gio.TlsCertificate | None = None self._peer_certificate_errors: set[Gio.TlsCertificateFlags] | None = None self._con: Connection | None = None self._local_address: Gio.SocketAddress | None = None self._remote_address: str | None = None self._mode: Mode = Mode.CLIENT self._fallback_ns: set[str] = set() self._tasks: list[Task] = [] 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 = StreamState.DISCONNECTED self._connection_check_id = self._enable_connection_check() def add_task(self, task: Task) -> None: self._tasks.append(task) def remove_task(self, task: Task, _context: Any) -> None: try: self._tasks.remove(task) except Exception: pass @property def log_context(self) -> str: assert self._log_context is not None return self._log_context @property def features(self) -> Features | None: return self._stream_features @property def resumeable(self) -> bool: assert self._smacks is not None return self._smacks.resumeable @property def sm_supported(self) -> bool: assert self._smacks is not None return self._smacks.sm_supported @property def lang(self) -> str: return self._lang def set_lang(self, lang: str) -> None: self._lang = lang @property def username(self) -> str | None: return self._username @property def domain(self) -> str | None: return self._domain @property def resource(self) -> str | None: return self._resource def set_username(self, username: str | None) -> None: self._username = username def set_domain(self, domain: str | None) -> None: self._domain = domain def set_resource(self, resource: str) -> None: self._resource = resource def set_mode(self, mode: Mode) -> None: self._mode = mode @property def custom_host(self) -> CustomHostT | None: return self._custom_host def set_custom_host( self, host_or_uri: str, protocol: ConnectionProtocol, type_: ConnectionType ) -> None: 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: list[Gio.TlsCertificate]) -> None: self._accepted_certificates = certificates @property def ignored_tls_errors(self) -> set[Gio.TlsCertificateFlags]: return set(self._ignored_tls_errors) def set_ignored_tls_errors( self, errors: set[Gio.TlsCertificateFlags] | None ) -> None: if errors is None: errors = set() self._ignored_tls_errors = errors @property def ignore_tls_errors(self) -> bool: return self._ignore_tls_errors @property def tls_version(self) -> int | None: assert self._con is not None return self._con.tls_version @property def ciphersuite(self) -> str | None: assert self._con is not None return self._con.ciphersuite def get_channel_binding_data( self, type_: Gio.TlsChannelBindingType ) -> bytes | None: assert self._con is not None return self._con.get_channel_binding_data(type_) def set_ignore_tls_errors(self, ignore: bool) -> None: self._ignore_tls_errors = ignore def set_password(self, password: str | None) -> None: assert self._sasl is not None self._sasl.set_password(password) @property def password(self) -> str | None: assert self._sasl is not None return self._sasl.password @property def peer_certificate( self, ) -> tuple[Gio.TlsCertificate | None, set[Gio.TlsCertificateFlags] | None]: if self._con is not None: return self._con.peer_certificate return self._peer_certificate, self._peer_certificate_errors @property def current_address(self) -> ServerAddress | None: return self._current_address @property def current_connection_type(self) -> ConnectionType: assert self._current_address is not None return self._current_address.type @property def is_websocket(self) -> bool: assert self._current_address is not None return self._current_address.protocol == ConnectionProtocol.WEBSOCKET @property def stream_id(self) -> str | None: return self._stream_id @property def is_stream_secure(self) -> bool: direct_tls = self.current_connection_type == ConnectionType.DIRECT_TLS return self._stream_secure or direct_tls @property def is_stream_authenticated(self) -> bool: return self._stream_authenticated @property def state(self) -> StreamState: return self._state @state.setter def state(self, value: StreamState) -> None: self._state = value self._log.info("Set state: %s", value) def set_state(self, state: StreamState) -> None: self.state = state self._xmpp_state_machine() @property def local_address(self) -> Gio.SocketAddress | None: return self._local_address @property def remote_address(self) -> str | None: return self._remote_address @property def connection_types(self) -> list[ConnectionType]: 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: list[ConnectionType]) -> None: self._allowed_con_types = con_types @property def mechs(self) -> set[str]: return set( self._allowed_mechs or {"SCRAM-SHA-512", "SCRAM-SHA-256", "SCRAM-SHA-1", "PLAIN"} ) def set_mechs(self, mechs: set[str]) -> None: self._allowed_mechs = mechs @property def protocols(self) -> list[ConnectionProtocol]: 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: list[ConnectionProtocol]) -> None: self._allowed_protocols = protocols def set_sm_disabled(self, value: bool) -> None: self._sm_disabled = value @property def sm_disabled(self) -> bool: return self._sm_disabled @property def client_cert(self) -> tuple[Any, str | None]: return self._client_cert, self._client_cert_pass def set_client_cert(self, client_cert: Any, client_cert_pass: str) -> None: self._client_cert = client_cert self._client_cert_pass = client_cert_pass def set_proxy(self, proxy: ProxyData | None) -> None: self._proxy = proxy @property def proxy(self) -> ProxyData | None: return self._proxy def set_host_meta_data(self, data: bytes) -> bytes | None: self._host_meta_data = data def get_host_meta_data(self) -> bytes | None: return self._host_meta_data def get_bound_jid(self) -> JID | None: return self._jid def _set_bound_jid(self, jid: str) -> None: self._jid = JID.from_string(jid) def set_supported_fallback_ns(self, ns: Sequence[str]) -> None: self._fallback_ns = set(ns) def get_supported_fallback_ns(self) -> set[str]: return set(self._fallback_ns) @property def has_error(self) -> bool: return self._error[0] is not None def get_error(self) -> tuple[StreamError | None, str | None, str | None]: return self._error def _reset_error(self) -> None: self._error = None, None, None def _set_error( self, domain: StreamError, error: str, text: str | None = None ) -> None: self._log.info("Set error: %s, %s, %s", domain, error, text) self._error = domain, error, text def _connect(self) -> None: 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._peer_certificate = None self._peer_certificate_errors = None 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.connect() def _get_connection(self, *args: Any) -> WebsocketConnection | TCPConnection: if self.is_websocket: return WebsocketConnection(*args) return TCPConnection(*args) def connect(self) -> None: 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_host_meta_data(self._host_meta_data) 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: ServerAddresses, _signal_name: str ) -> None: self._log.info("Domain resolved") self._log.info(self._addresses) self.state = StreamState.RESOLVED assert self._addresses is not None self._address_generator = self._addresses.get_next_address( self.connection_types, self.protocols ) self._try_next_ip() def _try_next_ip(self, *args: Any) -> None: try: assert self._address_generator is not None self._current_address = next(self._address_generator) except NoMoreAddresses: self._current_address = None self.state = StreamState.DISCONNECTED assert self._addresses is not None 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: bool = False) -> None: if self._state == StreamState.RESOLVE: assert self._addresses is not None 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 _start_stream_end_timeout(self) -> None: if self._stream_end_timeout_id is not None: return self._log.info("Start stream end timeout") self._stream_end_timeout_id = GLib.timeout_add_seconds( STREAM_END_TIMEOUT_SEC, self._on_stream_end_timeout ) def _remove_stream_end_timeout(self) -> None: if self._stream_end_timeout_id is None: return self._log.info("Remove stream end timeout") GLib.source_remove(self._stream_end_timeout_id) self._stream_end_timeout_id = None def _on_stream_end_timeout(self) -> None: self._stream_end_timeout_id = None if self._state != StreamState.DISCONNECTING: return self._log.warning("Timeout while waiting for stream end") self._con.shutdown_input() def _disconnect(self, immediate: bool = True) -> None: self._log.info("Disconnect immediate=%s", immediate) if immediate: self.state = StreamState.DISCONNECTING self._remove_connection_check_timeout() self._con.disconnect() else: self._start_stream_end_timeout() self._graceful_disconnect() def _graceful_disconnect(self) -> None: self.state = StreamState.DISCONNECTING self._remove_connection_check_timeout() assert self._smacks is not None self._smacks.close_session() self._end_stream() self._con.shutdown_output() def send(self, stanza: Protocol, *args: Any, **kwargs: Any) -> str: # Alias for backwards compat return self.send_stanza(stanza) def _on_connected(self, connection: Connection, _signal_name: str) -> None: self.set_state(StreamState.CONNECTED) self._local_address = connection.local_address self._remote_address = connection.remote_address def _on_disconnected(self, _connection: Connection, _signal_name: str) -> None: self.state = StreamState.DISCONNECTED for task in self._tasks: task.cancel() self._reset_stream() self.notify("disconnected") def _on_connection_failed(self, _connection: Connection, _signal_name: str) -> None: 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", ( "Unable to connect to last " f"successful address: {self._current_address}" ), ) self.notify("connection-failed") def _disconnect_with_error( self, error_domain: StreamError, error: str, text: str | None = None ) -> None: self._set_error(error_domain, error, text) self.disconnect() def _on_parsing_error( self, _dispatcher: StanzaDispatcher, _signal_name: str, error: str | None ) -> None: 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: StanzaDispatcher, _signal_name: str, error: str | None ) -> None: if not self.has_error: self._set_error(StreamError.STREAM, error or "stream-end") self._remove_stream_end_timeout() self._con.shutdown_input() if self._state not in (StreamState.DISCONNECTING, StreamState.DISCONNECTED): # If we are in DISCONNECTING or DISCONNECTED this means we initiated # the disconnect, so we dont need to call disconnect again. self._graceful_disconnect() def _reset_stream(self) -> None: 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) -> None: if self.is_websocket: nonza = WebsocketCloseHeader() else: nonza = "" self.send_nonza(nonza) @overload def get_module(self, name: Literal["Activity"]) -> Activity: ... @overload def get_module(self, name: Literal["AdHoc"]) -> AdHoc: ... @overload def get_module(self, name: Literal["Annotations"]) -> Annotations: ... @overload def get_module(self, name: Literal["Attention"]) -> Attention: ... @overload def get_module(self, name: Literal["Blocking"]) -> Blocking: ... @overload def get_module(self, name: Literal["NativeBookmarks"]) -> NativeBookmarks: ... @overload def get_module(self, name: Literal["PEPBookmarks"]) -> PEPBookmarks: ... @overload def get_module(self, name: Literal["PrivateBookmarks"]) -> PrivateBookmarks: ... @overload def get_module(self, name: Literal["Captcha"]) -> Captcha: ... @overload def get_module(self, name: Literal["ChatMarkers"]) -> ChatMarkers: ... @overload def get_module(self, name: Literal["Chatstates"]) -> Chatstates: ... @overload def get_module(self, name: Literal["Correction"]) -> Correction: ... @overload def get_module(self, name: Literal["Delay"]) -> Delay: ... @overload def get_module(self, name: Literal["Delimiter"]) -> Delimiter: ... @overload def get_module(self, name: Literal["Discovery"]) -> Discovery: ... @overload def get_module(self, name: Literal["EME"]) -> EME: ... @overload def get_module(self, name: Literal["EntityCaps"]) -> EntityCaps: ... @overload def get_module(self, name: Literal["EntityTime"]) -> EntityTime: ... @overload def get_module(self, name: Literal["Hats"]) -> Hats: ... @overload def get_module(self, name: Literal["HTTPAuth"]) -> HTTPAuth: ... @overload def get_module(self, name: Literal["HTTPUpload"]) -> HTTPUpload: ... @overload def get_module(self, name: Literal["IBB"]) -> IBB: ... @overload def get_module(self, name: Literal["Idle"]) -> Idle: ... @overload def get_module(self, name: Literal["BaseIq"]) -> BaseIq: ... @overload def get_module(self, name: Literal["LastActivity"]) -> LastActivity: ... @overload def get_module(self, name: Literal["Location"]) -> Location: ... @overload def get_module(self, name: Literal["MAM"]) -> MAM: ... @overload def get_module(self, name: Literal["MDS"]) -> MDS: ... @overload def get_module(self, name: Literal["BaseMessage"]) -> BaseMessage: ... @overload def get_module(self, name: Literal["Mood"]) -> Mood: ... @overload def get_module(self, name: Literal["MUC"]) -> MUC: ... @overload def get_module(self, name: Literal["Moderation"]) -> Moderation: ... @overload def get_module(self, name: Literal["Muclumbus"]) -> Muclumbus: ... @overload def get_module(self, name: Literal["Nickname"]) -> Nickname: ... @overload def get_module(self, name: Literal["OMEMO"]) -> OMEMO: ... @overload def get_module(self, name: Literal["OOB"]) -> OOB: ... @overload def get_module(self, name: Literal["OpenPGP"]) -> OpenPGP: ... @overload def get_module(self, name: Literal["PGPLegacy"]) -> PGPLegacy: ... @overload def get_module(self, name: Literal["Ping"]) -> Ping: ... @overload def get_module(self, name: Literal["BasePresence"]) -> BasePresence: ... @overload def get_module(self, name: Literal["PubSub"]) -> PubSub: ... @overload def get_module(self, name: Literal["Reactions"]) -> Reactions: ... @overload def get_module(self, name: Literal["Receipts"]) -> Receipts: ... @overload def get_module(self, name: Literal["Register"]) -> Register: ... @overload def get_module(self, name: Literal["Replies"]) -> Replies: ... @overload def get_module(self, name: Literal["Retraction"]) -> Retraction: ... @overload def get_module(self, name: Literal["Roster"]) -> Roster: ... @overload def get_module(self, name: Literal["SecurityLabels"]) -> SecurityLabels: ... @overload def get_module(self, name: Literal["SoftwareVersion"]) -> SoftwareVersion: ... @overload def get_module(self, name: Literal["Tune"]) -> Tune: ... @overload def get_module(self, name: Literal["UserAvatar"]) -> UserAvatar: ... @overload def get_module(self, name: Literal["VCard4"]) -> VCard4: ... @overload def get_module(self, name: Literal["VCardAvatar"]) -> VCardAvatar: ... @overload def get_module(self, name: Literal["VCardTemp"]) -> VCardTemp: ... def get_module(self, name: NBXMPPModuleNameT) -> NBXMPPModuleT: assert self._dispatcher is not None return self._dispatcher.get_module(name) def _on_bad_certificate(self, connection: Connection, _signal_name: str) -> None: self._peer_certificate, self._peer_certificate_errors = ( connection.peer_certificate ) self._set_error(StreamError.BAD_CERTIFICATE, "bad certificate") def accept_certificate(self) -> None: self._log.info("Certificate accepted") assert self._peer_certificate is not None self._accepted_certificates.append(self._peer_certificate) self._connect() def _on_data_sent( self, _connection: Connection, _signal_name: str, data: Any ) -> None: self.notify("stanza-sent", data) def _on_before_dispatch( self, _dispatcher: StanzaDispatcher, _signal_name: str, data: Any ) -> None: self.notify("stanza-received", data) def _on_data_received( self, _connection: Connection, _signal_name: str, data: str ) -> None: self._remove_connection_check_timeout() self._dispatcher.process_data(data) def _enable_connection_check(self) -> int: def _on_check() -> bool: if self._state == StreamState.ACTIVE: self.check_if_connected() return GLib.SOURCE_CONTINUE return GLib.timeout_add_seconds(CONNECTION_CHECK_INTERVAL, _on_check) def check_if_connected(self) -> None: if self._connection_check_timeout_id is not None: self._log.info( "Ignore connection check request, because request is already in progress" ) return if self._state != StreamState.ACTIVE: self._log.info( "Ignore connection check request, because stream state is %s", self._state, ) return self._log.info("Start connection check") self._connection_check_timeout_id = GLib.timeout_add_seconds( CONNECTION_CHECK_TIMEOUT, self._on_connection_check_timeout ) assert self._smacks is not None if self._smacks.sm_supported: self._smacks.request_ack() else: self.get_module("Ping").ping(self.domain, timeout=5) def _on_connection_check_timeout(self) -> None: self._connection_check_timeout_id = None if self._state == StreamState.ACTIVE: self._log.info("Connection check timeout") self.disconnect(immediate=True) def _remove_connection_check_timeout(self) -> None: if self._connection_check_timeout_id is None: return GLib.source_remove(self._connection_check_timeout_id) self._connection_check_timeout_id = None if self._state == StreamState.ACTIVE: self._log.info("Connection check successful") def send_stanza( self, stanza: Protocol | Any, now: bool = False, callback: Callable[..., Any] | None = None, timeout: int | None = None, user_data: Any = None, ) -> str: 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_ = generate_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) assert self._smacks is not None self._smacks.save_in_queue(stanza) return id_ def SendAndCallForResponse( self, stanza: Protocol, callback: Callable[..., Any], user_data: Any = None ) -> None: self.send_stanza(stanza, callback=callback, user_data=user_data) def send_nonza(self, nonza: Any, now: bool = False) -> None: self._con.send(nonza, now) def _xmpp_state_machine(self, stanza: Node | None = None) -> 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 self._stream_authenticated and self._mode.is_login_test: self.notify("login-successful") self.disconnect() return 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: assert self._sasl is not None self._sasl.delegate(stanza) elif self.state == StreamState.AUTH_SUCCESSFUL: self._stream_authenticated = True assert self._sasl is not None if self._sasl.is_sasl2(): self.state = StreamState.WAIT_FOR_FEATURES else: self._start_stream() elif self.state == StreamState.AUTH_FAILED: assert self._sasl is not None 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) assert self._smacks is not 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: assert self._smacks is not None 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: Features) -> None: if self.is_stream_authenticated: self._stream_features = features assert self._smacks is not None 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) -> None: 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) -> None: self.send_nonza(TLSRequest()) self.state = StreamState.WAIT_FOR_TLS_PROCEED def _start_auth(self, features: Features) -> None: if not features.has_sasl() and not features.has_sasl_2(): self._log.error("Server does not support SASL") self._disconnect_with_error(StreamError.SASL, "sasl-not-supported") return self.state = StreamState.PROCEED_WITH_AUTH assert self._sasl is not None self._sasl.start_auth(features) def _start_bind(self) -> None: 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: Protocol) -> None: 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: Protocol) -> None: 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 register_handler(self, *args: Any, **kwargs: Any) -> None: self._dispatcher.register_handler(*args, **kwargs) def unregister_handler(self, *args: Any, **kwargs: Any) -> None: self._dispatcher.unregister_handler(*args, **kwargs) def destroy(self) -> None: for task in self._tasks: task.cancel() self._smacks = None self._sasl = None self._dispatcher.cleanup() self._dispatcher = None self.remove_subscriptions() self._remove_stream_end_timeout() self._remove_connection_check_timeout() GLib.source_remove(self._connection_check_id) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/connection.py000066400000000000000000000122761524361513300255010ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from typing import Any import logging from gi.repository import Gio from nbxmpp.const import ConnectionType from nbxmpp.const import TCPState from nbxmpp.structs import ServerAddress from nbxmpp.util import LogAdapter from nbxmpp.util import min_version from nbxmpp.util import Observable log = logging.getLogger("nbxmpp.connection") class Connection(Observable): """ Base Connection Class Signals: data-sent data-received bad-certificate connection-failed disconnected """ def __init__( self, log_context: str | None, address: ServerAddress, accepted_certificates: list[Gio.TlsCertificate], ignore_tls_errors: bool, ignored_tls_errors: set[Gio.TlsCertificateFlags], client_cert: Any, ) -> None: self._log = LogAdapter(log, {"context": log_context}) Observable.__init__(self, self._log) self._client_cert = client_cert self._address = address self._local_address: Gio.SocketAddress | None = None self._remote_address: str | None = None self._state = TCPState.DISCONNECTED self._tls_con: Gio.TlsConnection | None = None self._peer_certificate: Gio.TlsCertificate | None = None self._peer_certificate_errors: set[Gio.TlsCertificateFlags] | None = None self._accepted_certificates = accepted_certificates self._ignore_tls_errors = ignore_tls_errors self._ignored_tls_errors = ignored_tls_errors @property def tls_version(self) -> int | None: if self._tls_con is None: return None if not min_version("GLib", "2.69.0"): return None return self._tls_con.get_protocol_version() @property def ciphersuite(self) -> str | None: if self._tls_con is None: return None if not min_version("GLib", "2.69.0"): return None return self._tls_con.get_ciphersuite_name() def get_channel_binding_data( self, type_: Gio.TlsChannelBindingType ) -> bytes | None: assert self._tls_con is not None try: success, data = self._tls_con.get_channel_binding_data(type_) except Exception as error: self._log.warning("Unable to get channel binding data: %s", error) return None if not success: return None return data @property def local_address(self) -> Gio.SocketAddress | None: return self._local_address @property def remote_address(self) -> str | None: return self._remote_address @property def peer_certificate( self, ) -> tuple[Gio.TlsCertificate | None, set[Gio.TlsCertificateFlags] | None]: return (self._peer_certificate, self._peer_certificate_errors) @property def connection_type(self) -> ConnectionType: assert self._address is not None return self._address.type @property def state(self) -> TCPState: return self._state @state.setter def state(self, value: TCPState) -> None: self._log.info("Set Connection State: %s", value) self._state = value def _accept_certificate(self) -> bool: 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: assert self._peer_certificate is not None if self._peer_certificate.is_same(accepted_certificate): self._peer_certificate_errors.discard( Gio.TlsCertificateFlags.UNKNOWN_CA ) break return bool(not self._peer_certificate_errors) def disconnect(self) -> None: raise NotImplementedError def connect(self) -> None: raise NotImplementedError def send(self, stanza: Any, now: bool = False) -> None: raise NotImplementedError def _log_stanza(self, data: str, received: bool = True) -> None: direction = "RECEIVED" if received else "SENT" message = "::::: DATA %s ::::\n\n%s\n" self._log.info(message, direction, data) def start_tls_negotiation(self) -> None: raise NotImplementedError def shutdown_output(self) -> None: raise NotImplementedError def shutdown_input(self) -> None: raise NotImplementedError def destroy(self) -> None: self.remove_subscriptions() self._peer_certificate = None self._client_cert = None self._address = None self._tls_con = None python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/const.py000066400000000000000000000331641524361513300244670ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-2.0-or-later from __future__ import annotations from enum import Enum from enum import IntEnum from functools import total_ordering from gi.repository import Gio from gi.repository import GLib from packaging.version import Version GLIB_VERSION = Version( f"{GLib.MAJOR_VERSION}.{GLib.MINOR_VERSION}.{GLib.MICRO_VERSION}" ) class IqType(Enum): GET = "get" SET = "set" RESULT = "result" ERROR = "error" @property def is_get(self) -> bool: return self == IqType.GET @property def is_set(self) -> bool: return self == IqType.SET @property def is_result(self) -> bool: return self == IqType.RESULT @property def is_error(self) -> bool: return self == IqType.ERROR class MessageType(Enum): NORMAL = "normal" CHAT = "chat" GROUPCHAT = "groupchat" HEADLINE = "headline" ERROR = "error" @property def is_normal(self) -> bool: return self == MessageType.NORMAL @property def is_chat(self) -> bool: return self == MessageType.CHAT @property def is_groupchat(self) -> bool: return self == MessageType.GROUPCHAT @property def is_headline(self) -> bool: return self == MessageType.HEADLINE @property def is_error(self) -> bool: 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) -> bool: return self == PresenceType.AVAILABLE @property def is_unavailable(self) -> bool: return self == PresenceType.UNAVAILABLE @property def is_error(self) -> bool: return self == PresenceType.ERROR @property def is_probe(self) -> bool: return self == PresenceType.PROBE @property def is_unsubscribe(self) -> bool: return self == PresenceType.UNSUBSCRIBE @property def is_unsubscribed(self) -> bool: return self == PresenceType.UNSUBSCRIBED @property def is_subscribe(self) -> bool: return self == PresenceType.SUBSCRIBE @property def is_subscribed(self) -> bool: return self == PresenceType.SUBSCRIBED @total_ordering class PresenceShow(Enum): ONLINE = "online" CHAT = "chat" AWAY = "away" XA = "xa" DND = "dnd" @property def is_online(self) -> bool: return self == PresenceShow.ONLINE @property def is_chat(self) -> bool: return self == PresenceShow.CHAT @property def is_away(self) -> bool: return self == PresenceShow.AWAY @property def is_xa(self) -> bool: return self == PresenceShow.XA @property def is_dnd(self) -> bool: return self == PresenceShow.DND def __lt__(self, other: PresenceShow) -> bool: try: w1 = self._WEIGHTS[self] w2 = self._WEIGHTS[other] except KeyError: return NotImplemented return w1 < w2 PresenceShow._WEIGHTS = { PresenceShow.DND: 2, PresenceShow.CHAT: 1, PresenceShow.ONLINE: 0, PresenceShow.AWAY: -1, PresenceShow.XA: -2, } @total_ordering class Chatstate(Enum): COMPOSING = "composing" PAUSED = "paused" ACTIVE = "active" INACTIVE = "inactive" GONE = "gone" @property def is_composing(self) -> bool: return self == Chatstate.COMPOSING @property def is_paused(self) -> bool: return self == Chatstate.PAUSED @property def is_active(self) -> bool: return self == Chatstate.ACTIVE @property def is_inactive(self) -> bool: return self == Chatstate.INACTIVE @property def is_gone(self) -> bool: return self == Chatstate.GONE def __lt__(self, other: Chatstate) -> bool: try: w1 = self._WEIGHTS[self] w2 = self._WEIGHTS[other] except KeyError: return NotImplemented return w1 < w2 Chatstate._WEIGHTS = { Chatstate.COMPOSING: 0, Chatstate.PAUSED: 1, Chatstate.ACTIVE: 2, Chatstate.INACTIVE: 3, Chatstate.GONE: 4, } 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) -> bool: return self == Affiliation.OWNER @property def is_admin(self) -> bool: return self == Affiliation.ADMIN @property def is_member(self) -> bool: return self == Affiliation.MEMBER @property def is_outcast(self) -> bool: return self == Affiliation.OUTCAST @property def is_none(self) -> bool: return self == Affiliation.NONE def __lt__(self, other: Affiliation) -> bool: 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) -> bool: return self == Role.MODERATOR @property def is_participant(self) -> bool: return self == Role.PARTICIPANT @property def is_visitor(self) -> bool: return self == Role.VISITOR @property def is_none(self) -> bool: return self == Role.NONE def __lt__(self, other: Role) -> bool: 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) -> bool: return self == ConnectionType.DIRECT_TLS @property def is_start_tls(self) -> bool: return self == ConnectionType.START_TLS @property def is_plain(self) -> bool: 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) -> bool: return self == Mode.CLIENT @property def is_register(self) -> bool: return self == Mode.REGISTER @property def is_login_test(self) -> bool: return self == Mode.LOGIN_TEST @property def is_anonymous_test(self) -> bool: 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"] REGISTER_FIELDS = [ "username", "nick", "password", "name", "first", "last", "email", "address", "city", "state", "zip", "phone", "url", "date", ] 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", } NOT_ALLOWED_XML_CHARS = { "&": "&", "<": "<", ">": ">", '"': """, "\x0C": "", "\x1B": "", } python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/dispatcher.py000066400000000000000000000672401524361513300254710ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import Literal from typing import overload from typing import TYPE_CHECKING import logging import time from collections.abc import Callable from xml.parsers.expat import ExpatError from gi.repository import GLib from nbxmpp.exceptions import StanzaDecrypted from nbxmpp.modules.activity import Activity from nbxmpp.modules.adhoc import AdHoc from nbxmpp.modules.annotations import Annotations from nbxmpp.modules.attention import Attention from nbxmpp.modules.blocking import Blocking from nbxmpp.modules.bookmarks.native_bookmarks import NativeBookmarks from nbxmpp.modules.bookmarks.pep_bookmarks import PEPBookmarks from nbxmpp.modules.bookmarks.private_bookmarks import PrivateBookmarks from nbxmpp.modules.captcha import Captcha from nbxmpp.modules.chat_markers import ChatMarkers from nbxmpp.modules.chatstates import Chatstates from nbxmpp.modules.correction import Correction from nbxmpp.modules.delay import Delay from nbxmpp.modules.delimiter import Delimiter from nbxmpp.modules.discovery import Discovery from nbxmpp.modules.eme import EME from nbxmpp.modules.entity_caps import EntityCaps from nbxmpp.modules.entity_time import EntityTime from nbxmpp.modules.http_auth import HTTPAuth from nbxmpp.modules.http_upload import HTTPUpload from nbxmpp.modules.ibb import IBB from nbxmpp.modules.idle import Idle from nbxmpp.modules.iq import BaseIq from nbxmpp.modules.last_activity import LastActivity from nbxmpp.modules.location import Location from nbxmpp.modules.mam import MAM from nbxmpp.modules.mds import MDS from nbxmpp.modules.message import BaseMessage from nbxmpp.modules.misc import unwrap_carbon from nbxmpp.modules.misc import unwrap_mam from nbxmpp.modules.mood import Mood from nbxmpp.modules.muc import MUC from nbxmpp.modules.muc.hats import Hats from nbxmpp.modules.muc.moderation import Moderation from nbxmpp.modules.muclumbus import Muclumbus from nbxmpp.modules.nickname import Nickname from nbxmpp.modules.ogp import OpenGraph from nbxmpp.modules.omemo import OMEMO from nbxmpp.modules.oob import OOB from nbxmpp.modules.openpgp import OpenPGP from nbxmpp.modules.pgplegacy import PGPLegacy from nbxmpp.modules.ping import Ping from nbxmpp.modules.presence import BasePresence from nbxmpp.modules.pubsub import PubSub from nbxmpp.modules.reactions import Reactions from nbxmpp.modules.receipts import Receipts from nbxmpp.modules.register import Register from nbxmpp.modules.replies import Replies from nbxmpp.modules.retraction import Retraction from nbxmpp.modules.roster import Roster from nbxmpp.modules.security_labels import SecurityLabels from nbxmpp.modules.software_version import SoftwareVersion from nbxmpp.modules.tune import Tune from nbxmpp.modules.user_avatar import UserAvatar from nbxmpp.modules.vcard4 import VCard4 from nbxmpp.modules.vcard_avatar import VCardAvatar from nbxmpp.modules.vcard_temp import VCardTemp from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_FEATURE_NOT_IMPLEMENTED from nbxmpp.protocol import Error from nbxmpp.protocol import InvalidFrom from nbxmpp.protocol import InvalidJid from nbxmpp.protocol import InvalidStanza from nbxmpp.protocol import Iq from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Presence from nbxmpp.protocol import Protocol from nbxmpp.protocol import StreamErrorNode from nbxmpp.simplexml import Node from nbxmpp.simplexml import NodeBuilder from nbxmpp.structs import StanzaHandler from nbxmpp.util import get_properties_struct from nbxmpp.util import INVALID_XML_RX from nbxmpp.util import is_websocket_close from nbxmpp.util import is_websocket_stream_error from nbxmpp.util import LogAdapter from nbxmpp.util import Observable if TYPE_CHECKING: from nbxmpp.client import Client log = logging.getLogger("nbxmpp.dispatcher") NBXMPPModuleNameT = Literal[ "Activity", "Activity", "AdHoc", "Annotations", "Attention", "BasePresence", "BaseMessage", "BaseIq", "Blocking", "Captcha", "ChatMarkers", "Chatstates", "Correction", "Delay", "Delimiter", "Discovery", "EME", "EntityCaps", "EntityTime", "Hats", "HTTPAuth", "HTTPUpload", "IBB", "Idle", "LastActivity", "Location", "MAM", "MDS", "Moderation", "Mood", "MUC", "Muclumbus", "NativeBookmarks", "Nickname", "OMEMO", "OOB", "OpenPGP", "PEPBookmarks", "PGPLegacy", "Ping", "PrivateBookmarks", "PubSub", "Reactions", "Receipts", "Register", "Replies", "Retraction", "Roster", "SecurityLabels", "SoftwareVersion", "Tune", "UserAvatar", "VCardAvatar", "VCardTemp", "VCard4", ] NBXMPPModuleT = ( Activity | AdHoc | Annotations | Attention | BasePresence | BaseMessage | BaseIq | Blocking | Captcha | ChatMarkers | Chatstates | Correction | Delay | Delimiter | Discovery | EME | EntityCaps | EntityTime | Hats | HTTPAuth | HTTPUpload | IBB | Idle | LastActivity | Location | MAM | MDS | Moderation | Mood | MUC | Muclumbus | NativeBookmarks | Nickname | OMEMO | OOB | OpenGraph | OpenPGP | PEPBookmarks | PGPLegacy | Ping | PrivateBookmarks | PubSub | Reactions | Receipts | Register | Replies | Retraction | Roster | SecurityLabels | SoftwareVersion | Tune | UserAvatar | VCardAvatar | VCardTemp | VCard4 ) class StanzaDispatcher(Observable): """ Dispatches stanzas to handlers Signals: before-dispatch parsing-error stream-end """ def __init__(self, client: Client) -> None: Observable.__init__(self, log) self._client = client self._modules: dict[str, NBXMPPModuleT] = {} self._parser: NodeBuilder | None = None self._websocket_stream_error: str | None = None self._log = LogAdapter(log, {"context": client.log_context}) self._handlers: dict[str, dict[str, dict[str, Any]]] = {} self._id_callbacks: dict[str, tuple[Callable[..., Any], float | None, Any]] = {} self._dispatch_callback: Callable[..., Any] | None = None self._timeout_id: int | None = None self._stanza_types = { "iq": Iq, "message": Message, "presence": Presence, "error": StreamErrorNode, } 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: Callable[..., Any]) -> None: self._log.info("Set dispatch callback: %s", callback) self._dispatch_callback = callback @overload def get_module(self, name: Literal["Activity"]) -> Activity: ... @overload def get_module(self, name: Literal["AdHoc"]) -> AdHoc: ... @overload def get_module(self, name: Literal["Annotations"]) -> Annotations: ... @overload def get_module(self, name: Literal["Attention"]) -> Attention: ... @overload def get_module(self, name: Literal["Blocking"]) -> Blocking: ... @overload def get_module(self, name: Literal["NativeBookmarks"]) -> NativeBookmarks: ... @overload def get_module(self, name: Literal["PEPBookmarks"]) -> PEPBookmarks: ... @overload def get_module(self, name: Literal["PrivateBookmarks"]) -> PrivateBookmarks: ... @overload def get_module(self, name: Literal["Captcha"]) -> Captcha: ... @overload def get_module(self, name: Literal["ChatMarkers"]) -> ChatMarkers: ... @overload def get_module(self, name: Literal["Chatstates"]) -> Chatstates: ... @overload def get_module(self, name: Literal["Correction"]) -> Correction: ... @overload def get_module(self, name: Literal["Delay"]) -> Delay: ... @overload def get_module(self, name: Literal["Delimiter"]) -> Delimiter: ... @overload def get_module(self, name: Literal["Discovery"]) -> Discovery: ... @overload def get_module(self, name: Literal["EME"]) -> EME: ... @overload def get_module(self, name: Literal["EntityCaps"]) -> EntityCaps: ... @overload def get_module(self, name: Literal["EntityTime"]) -> EntityTime: ... @overload def get_module(self, name: Literal["Hats"]) -> Hats: ... @overload def get_module(self, name: Literal["HTTPAuth"]) -> HTTPAuth: ... @overload def get_module(self, name: Literal["HTTPUpload"]) -> HTTPUpload: ... @overload def get_module(self, name: Literal["IBB"]) -> IBB: ... @overload def get_module(self, name: Literal["Idle"]) -> Idle: ... @overload def get_module(self, name: Literal["BaseIq"]) -> BaseIq: ... @overload def get_module(self, name: Literal["LastActivity"]) -> LastActivity: ... @overload def get_module(self, name: Literal["Location"]) -> Location: ... @overload def get_module(self, name: Literal["MAM"]) -> MAM: ... @overload def get_module(self, name: Literal["MDS"]) -> MDS: ... @overload def get_module(self, name: Literal["BaseMessage"]) -> BaseMessage: ... @overload def get_module(self, name: Literal["Mood"]) -> Mood: ... @overload def get_module(self, name: Literal["MUC"]) -> MUC: ... @overload def get_module(self, name: Literal["Moderation"]) -> Moderation: ... @overload def get_module(self, name: Literal["Muclumbus"]) -> Muclumbus: ... @overload def get_module(self, name: Literal["Nickname"]) -> Nickname: ... @overload def get_module(self, name: Literal["OMEMO"]) -> OMEMO: ... @overload def get_module(self, name: Literal["OOB"]) -> OOB: ... @overload def get_module(self, name: Literal["OpenGraph"]) -> OpenGraph: ... @overload def get_module(self, name: Literal["OpenPGP"]) -> OpenPGP: ... @overload def get_module(self, name: Literal["PGPLegacy"]) -> PGPLegacy: ... @overload def get_module(self, name: Literal["Ping"]) -> Ping: ... @overload def get_module(self, name: Literal["BasePresence"]) -> BasePresence: ... @overload def get_module(self, name: Literal["PubSub"]) -> PubSub: ... @overload def get_module(self, name: Literal["Reactions"]) -> Reactions: ... @overload def get_module(self, name: Literal["Receipts"]) -> Receipts: ... @overload def get_module(self, name: Literal["Register"]) -> Register: ... @overload def get_module(self, name: Literal["Replies"]) -> Replies: ... @overload def get_module(self, name: Literal["Retraction"]) -> Retraction: ... @overload def get_module(self, name: Literal["Roster"]) -> Roster: ... @overload def get_module(self, name: Literal["SecurityLabels"]) -> SecurityLabels: ... @overload def get_module(self, name: Literal["SoftwareVersion"]) -> SoftwareVersion: ... @overload def get_module(self, name: Literal["Tune"]) -> Tune: ... @overload def get_module(self, name: Literal["UserAvatar"]) -> UserAvatar: ... @overload def get_module(self, name: Literal["VCard4"]) -> VCard4: ... @overload def get_module(self, name: Literal["VCardAvatar"]) -> VCardAvatar: ... @overload def get_module(self, name: Literal["VCardTemp"]) -> VCardTemp: ... def get_module(self, name: NBXMPPModuleNameT) -> NBXMPPModuleT: return self._modules[name] def _register_modules(self): assert self._client is not None self._modules["Activity"] = Activity(self._client) self._modules["AdHoc"] = AdHoc(self._client) self._modules["Annotations"] = Annotations(self._client) self._modules["Attention"] = Attention(self._client) self._modules["BasePresence"] = BasePresence(self._client) self._modules["BaseMessage"] = BaseMessage(self._client) self._modules["BaseIq"] = BaseIq(self._client) self._modules["Blocking"] = Blocking(self._client) self._modules["Captcha"] = Captcha(self._client) self._modules["ChatMarkers"] = ChatMarkers(self._client) self._modules["Chatstates"] = Chatstates(self._client) self._modules["Correction"] = Correction(self._client) self._modules["Delay"] = Delay(self._client) self._modules["Delimiter"] = Delimiter(self._client) self._modules["Discovery"] = Discovery(self._client) self._modules["EME"] = EME(self._client) self._modules["EntityCaps"] = EntityCaps(self._client) self._modules["EntityTime"] = EntityTime(self._client) self._modules["Hats"] = Hats(self._client) self._modules["HTTPAuth"] = HTTPAuth(self._client) self._modules["HTTPUpload"] = HTTPUpload(self._client) self._modules["IBB"] = IBB(self._client) self._modules["Idle"] = Idle(self._client) self._modules["LastActivity"] = LastActivity(self._client) self._modules["Location"] = Location(self._client) self._modules["MAM"] = MAM(self._client) self._modules["MDS"] = MDS(self._client) self._modules["Moderation"] = Moderation(self._client) self._modules["Mood"] = Mood(self._client) self._modules["MUC"] = MUC(self._client) self._modules["Muclumbus"] = Muclumbus(self._client) self._modules["NativeBookmarks"] = NativeBookmarks(self._client) self._modules["Nickname"] = Nickname(self._client) self._modules["OMEMO"] = OMEMO(self._client) self._modules["OOB"] = OOB(self._client) self._modules["OpenGraph"] = OpenGraph(self._client) self._modules["OpenPGP"] = OpenPGP(self._client) self._modules["PEPBookmarks"] = PEPBookmarks(self._client) self._modules["PGPLegacy"] = PGPLegacy(self._client) self._modules["Ping"] = Ping(self._client) self._modules["PrivateBookmarks"] = PrivateBookmarks(self._client) self._modules["PubSub"] = PubSub(self._client) self._modules["Reactions"] = Reactions(self._client) self._modules["Receipts"] = Receipts(self._client) self._modules["Register"] = Register(self._client) self._modules["Replies"] = Replies(self._client) self._modules["Retraction"] = Retraction(self._client) self._modules["Roster"] = Roster(self._client) self._modules["SecurityLabels"] = SecurityLabels(self._client) self._modules["SoftwareVersion"] = SoftwareVersion(self._client) self._modules["Tune"] = Tune(self._client) self._modules["UserAvatar"] = UserAvatar(self._client) self._modules["VCardAvatar"] = VCardAvatar(self._client) self._modules["VCardTemp"] = VCardTemp(self._client) self._modules["VCard4"] = VCard4(self._client) for instance in self._modules.values(): for handler in instance.handlers: self.register_handler(handler) def reset_parser(self) -> None: 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: str) -> str: return INVALID_XML_RX.sub("\ufffd", data) def process_data(self, data: str) -> None: # Parse incoming data data = self.replace_non_character(data) if self._client.is_websocket: try: stanza = Node(node=data) except (ExpatError, ValueError) as error: self._log.error("XML parsing error: %s", error) self.notify("parsing-error", str(error)) return 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", str(error)) return # end stream:stream tag received if self._parser.has_received_endtag(): self._log.info("End of stream: %s", self._parser.stream_error) self.notify("stream-end", self._parser.stream_error) return def _register_namespace(self, xmlns: str) -> None: """ 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: str, protocol: Any, xmlns: str | None = None ) -> 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: StanzaHandler) -> None: """ 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: StanzaHandler) -> None: """ 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: Protocol) -> None: """ 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: Protocol) -> None: 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 not typ: if name == "message": typ = "normal" elif name == "presence": typ = "available" else: typ = "" # 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 props = stanza.getProperties() self._log.debug("type: %s, properties: %s", typ, props) chain = self._build_handler_chain(xmlns, name, typ, props) try: self._execute_handler_chain(chain, stanza, properties) except StanzaDecrypted: props = stanza.getProperties() self._log.debug("type: %s, properties after decryption: %s", typ, props) chain = self._build_handler_chain( xmlns, name, typ, props, after_decryption=True ) self._execute_handler_chain(chain, stanza, properties) def _build_handler_chain( self, xmlns: str, name: str, typ: str, props: Any, *, after_decryption: bool = False, ) -> list[dict[str, Any]]: # Gather specifics depending on stanza properties specifics = ["default"] if typ and typ in self._handlers[xmlns][name]: specifics.append(typ) for prop in 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: list[dict[str, Any]] = [] 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"]) if after_decryption: # Filter everything out which was executed before decryption # so it is not executed again chain = list(filter(lambda x: x["priority"] > 9, chain)) return chain def _execute_handler_chain( self, chain: Any, stanza: Protocol, properties: Any ) -> None: for handler in chain: self._log.info("Call handler: %s", handler["func"].__qualname__) try: handler["func"](self._client, stanza, properties) except NodeProcessed: return except StanzaDecrypted: raise 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_: str, func: Callable[..., Any], timeout: float | None, user_data: Any ) -> None: 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) -> bool: self._log.info("Run timeout check") timeouts: dict[str, tuple[Callable[..., Any], float | None, Any]] = {} 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) -> None: if self._timeout_id is not None: GLib.source_remove(self._timeout_id) self._timeout_id = None def remove_iq_callback(self, id_: str) -> None: self._id_callbacks.pop(id_, None) def clear_iq_callbacks(self) -> None: self._log.info("Clear IQ callbacks") self._id_callbacks.clear() def cleanup(self) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/errors.py000066400000000000000000000111411524361513300246440ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any import logging from nbxmpp.modules.dataforms import MultipleDataForm from nbxmpp.modules.dataforms import SimpleDataForm from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Protocol from nbxmpp.structs import RegisterData def is_error(error: Any) -> bool: return isinstance(error, BaseError) class BaseError(Exception): def __init__(self, is_fatal: bool = False) -> None: self.is_fatal = is_fatal self.text = "" def __str__(self) -> str: return self.text def get_text(self, _pref_lang: str | None = None) -> str: return self.text class StanzaError(BaseError): log_level = logging.INFO app_namespace = None def __init__(self, stanza: Protocol) -> None: BaseError.__init__(self) self.stanza = stanza self._stanza_name = stanza.getName() self._error_node = stanza.getTag("error") self.condition: str | None = 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: dict[str, str] = {} 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) -> str | None: 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: str | None = None) -> str: 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: str, text: str) -> None: self._text[lang] = text def __str__(self) -> str: 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) -> float | None: 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) -> str | None: if self.app_condition != "retry": return None return self._error_node.getTagAttr("stamp") class MalformedStanzaError(BaseError): log_level = logging.WARNING def __init__(self, text: str, stanza: Protocol, is_fatal: bool = True) -> None: BaseError.__init__(self, is_fatal=is_fatal) self.stanza = stanza self.text = str(text) class CancelledError(BaseError): log_level = logging.INFO def __init__(self) -> None: BaseError.__init__(self, is_fatal=True) self.text = "Task has been cancelled" class TimeoutStanzaError(BaseError): log_level = logging.INFO def __init__(self) -> None: BaseError.__init__(self) self.text = "Timeout reached" class RegisterStanzaError(StanzaError): def __init__(self, stanza: Protocol, data: RegisterData) -> None: StanzaError.__init__(self, stanza) self._data = data def get_data(self) -> RegisterData: return self._data class ChangePasswordStanzaError(StanzaError): def __init__( self, stanza: Protocol, form: SimpleDataForm | MultipleDataForm ) -> None: StanzaError.__init__(self, stanza) self._form = form def get_form(self) -> SimpleDataForm | MultipleDataForm: return self._form python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/exceptions.py000066400000000000000000000005101524361513300255070ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later class EndOfConnection(Exception): pass class NonFatalSSLError(Exception): pass class FallbackLanguageError(Exception): pass class StanzaDecrypted(Exception): pass python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/language.py000066400000000000000000000176451524361513300251320ustar00rootroot00000000000000from __future__ import annotations from typing import Any from typing import cast import functools from collections.abc import Iterable from collections.abc import Iterator from collections.abc import Sequence # Copied from https://codeberg.org/jssfr/aioxmpp/src/branch/devel/aioxmpp/structs.py @functools.total_ordering class LanguageTag: """ Implementation of a language tag. This may be a fully RFC5646 compliant implementation some day, but for now it is only very simplistic stub. There is no input validation of any kind. """ __slots__ = ("_tag",) def __init__(self, *, tag: str): if not tag: raise ValueError("tag cannot be empty") self._tag = tag @property def match_str(self) -> str: """ The string which is used for matching two language tags. This is the lower-cased version of `print_str`. """ return self._tag.lower() @property def print_str(self) -> str: """ The stringified language tag. """ return self._tag @classmethod def fromstr(cls, s: str) -> LanguageTag: """ Create a language tag from the given string `s`. """ return cls(tag=s) def __str__(self) -> str: return self.print_str def __eq__(self, other: Any) -> bool: try: return self.match_str == other.match_str except AttributeError: return False def __lt__(self, other: Any) -> bool: try: return self.match_str < other.match_str except AttributeError: return NotImplemented def __le__(self, other: Any) -> bool: try: return self.match_str <= other.match_str except AttributeError: return NotImplemented def __hash__(self) -> int: return hash(self.match_str) def __repr__(self) -> str: return "<{}.{}.fromstr({!r})>".format( type(self).__module__, type(self).__qualname__, str(self) ) class LanguageRange: """ Implementation of a language range. This may be a fully RFC4647 compliant implementation some day, but for now it is only very simplistic stub. There is no input validation of any kind. LanguageRange instances compare and hash case-insensitively. """ __slots__ = ("_tag",) WILDCARD: LanguageRange def __init__(self, *, tag: str): if not tag: raise ValueError("range cannot be empty") self._tag = tag @property def match_str(self) -> str: """ The string which is used for matching two language tags. This is the lower-cased version of the `print_str`. """ return self._tag.lower() @property def print_str(self) -> str: """ The stringified language tag. """ return self._tag @classmethod def fromstr(cls, s: str) -> LanguageRange: """ Create a language tag from the given string `s`. """ if s == "*": return cls.WILDCARD return cls(tag=s) def __str__(self) -> str: return self.print_str def __eq__(self, other: Any) -> bool: try: return self.match_str == other.match_str except AttributeError: return False def __hash__(self) -> int: return hash(self.match_str) def __repr__(self) -> str: return "<{}.{}.fromstr({!r})>".format( type(self).__module__, type(self).__qualname__, str(self) ) def strip_rightmost(self) -> LanguageRange: """ Strip the rightmost part of the language range. If the new rightmost part is a singleton or ``x`` (i.e. starts an extension or private use part), it is also stripped. Return the newly created `LanguageRange`. """ parts = self.print_str.split("-") parts.pop() if parts and len(parts[-1]) == 1: parts.pop() return type(self).fromstr("-".join(parts)) LanguageRange.WILDCARD = LanguageRange(tag="*") def basic_filter_languages( languages: Sequence[LanguageTag], ranges: Iterable[LanguageRange] ) -> Iterator[LanguageTag]: """ Filter languages using the string-based basic filter algorithm described in RFC4647. `languages` must be a sequence of `LanguageTag` instances which are to be filtered. `ranges` must be an iterable which represent the basic language ranges to filter with, in priority order. The language ranges must be given as `LanguageRange` objects. Return an iterator of languages which matched any of the `ranges`. The sequence produced by the iterator is in match order and duplicate-free. The first range to match a language yields the language into the iterator, no other range can yield that language afterwards. """ if LanguageRange.WILDCARD in ranges: yield from languages return found: set[LanguageTag] = set() for language_range in ranges: range_str = language_range.match_str for language in languages: if language in found: continue match_str = language.match_str if match_str == range_str: yield language found.add(language) continue if len(range_str) < len(match_str): if ( match_str[: len(range_str)] == range_str and match_str[len(range_str)] == "-" ): yield language found.add(language) continue def lookup_language( languages: Sequence[LanguageTag], ranges: Iterable[LanguageRange] ) -> LanguageTag | None: """ Look up a single language in the sequence `languages` using the lookup mechanism described in RFC4647. If no match is found, `None` is returned. Otherwise, the first matching language is returned. `languages` must be a sequence of `LanguageTag` objects, while `ranges` must be an iterable of `LanguageRange` objects. """ for language_range in ranges: while True: try: return next(iter(basic_filter_languages(languages, [language_range]))) except StopIteration: pass try: language_range = language_range.strip_rightmost() except ValueError: break return None class LanguageMap(dict[LanguageTag | None, Any]): """ A `dict` subclass specialized for holding `LanugageTag` instances as keys. In addition to the interface provided by `dict`, instances of this class also have the following methods: """ def lookup(self, language_ranges: Sequence[LanguageRange]) -> Any: """ Perform an RFC4647 language range lookup on the keys in the dictionary. `language_ranges` must be a sequence of `LanguageRange` instances. Return the entry in the dictionary with a key as produced by `lookup_language`. If `lookup_language` does not find a match and the mapping contains an entry with key `None`, that entry is returned, otherwise `KeyError` is raised. """ keys = list(self.keys()) try: keys.remove(None) except ValueError: pass keys = cast(list[LanguageTag], keys) keys.sort() key = lookup_language(keys, language_ranges) return self[key] def any(self) -> Any: """ Returns any element from the language map, preferring the `None` key if it is available. Guarantees to always return the same element for a map with the same keys, even if the keys are iterated over in a different order. """ if not self: raise ValueError("any() on empty map") try: return self[None] except KeyError: return self[min(self)] # type: ignore python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/000077500000000000000000000000001524361513300244305ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/__init__.py000066400000000000000000000000001524361513300265270ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/account_invite.py000066400000000000000000000024401524361513300300140ustar00rootroot00000000000000# Copyright (C) 2026 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations import datetime as dt from nbxmpp.modules.dataforms import SimpleDataForm from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.protocol import Node from nbxmpp.structs import AccountInviteResult def parse_account_invite(form: Node) -> AccountInviteResult: form = SimpleDataForm(extend=form) assert isinstance(form, SimpleDataForm) # TODO: Check form type once its standardized # field = form.vars.get("FORM_TYPE") # if field is None: # return None try: uri = form["uri"].value except Exception: raise ValueError("uri field is missing") assert isinstance(uri, str) if not uri: raise ValueError("empty uri value") try: landing_url = form["landing-url"].value or None except Exception: landing_url = None assert isinstance(landing_url, str | None) try: expire = parse_datetime(form["expire"].value) except Exception: expire = None assert isinstance(expire, dt.datetime | None) return AccountInviteResult( uri=uri, landing_url=landing_url, expire=expire, ) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/activity.py000066400000000000000000000064631524361513300266470ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import ACTIVITIES from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import ActivityData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Activity(BaseModule): _depends = {"publish": "PubSub"} def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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.getChildren(): 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: Node) -> str | None: 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: ActivityData): 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/adhoc.py000066400000000000000000000153751524361513300260730ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import AdHocAction from nbxmpp.const import AdHocNoteType from nbxmpp.const import AdHocStatus from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.dataforms import MultipleDataForm from nbxmpp.modules.dataforms import SimpleDataForm from nbxmpp.modules.discovery import get_disco_request from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import Node from nbxmpp.protocol import Protocol from nbxmpp.structs import AdHocCommand from nbxmpp.structs import AdHocCommandNote from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class AdHoc(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [] @iq_request_task def request_command_list(self, jid: str | None = 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) children = response.getQueryChildren() command_list: list[AdHocCommand] = [] for item in children: 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: AdHocCommand, action: AdHocAction | None = None, dataform: SimpleDataForm | MultipleDataForm | None = 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: AdHocCommand, attrs: dict[str, Node | str], dataform: SimpleDataForm | MultipleDataForm | None, ) -> Iq: 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: Protocol) -> list[AdHocCommandNote]: notes: list[AdHocCommandNote] = [] 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: Protocol) -> tuple[set[AdHocAction], AdHocAction | None]: 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[AdHocAction] = 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: AdHocStatus, sent_sessionid: str | None) -> bool: # 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/annotations.py000066400000000000000000000061671524361513300273510ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import Node from nbxmpp.structs import AnnotationNote from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Annotations(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [] @property def domain(self) -> str | None: 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: list[AnnotationNote] = [] 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: list[AnnotationNote]): _task = yield for note in notes: self._log.info(note) response = yield _make_set_request(notes) yield process_response(response) def _make_request() -> Iq: payload = Node("storage", attrs={"xmlns": Namespace.ROSTERNOTES}) return Iq(typ="get", queryNS=Namespace.PRIVATE, payload=payload) def _make_set_request(notes: list[AnnotationNote]) -> Iq: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/attention.py000066400000000000000000000025701524361513300270130ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Attention(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/base.py000066400000000000000000000015601524361513300257160ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import TYPE_CHECKING import logging from nbxmpp.util import LogAdapter if TYPE_CHECKING: from nbxmpp.client import Client class BaseModule: _depends: dict[str, str] = {} def __init__(self, client: Client) -> None: logger_name = "nbxmpp.m.%s" % self.__class__.__name__.lower() self._log = LogAdapter( logging.getLogger(logger_name), {"context": client.log_context} ) def __getattr__(self, name: str) -> Any: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/bits_of_binary.py000066400000000000000000000034431524361513300277770ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later import hashlib import logging from nbxmpp.namespaces import Namespace from nbxmpp.simplexml import Node from nbxmpp.structs import BobData from nbxmpp.util import b64decode log = logging.getLogger("nbxmpp.m.bob") def parse_bob_data(stanza: Node) -> BobData | None: 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 assert max_age is not 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) 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/blocking.py000066400000000000000000000104701524361513300265740ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from collections.abc import Generator from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.simplexml import Node from nbxmpp.structs import BlockingProperties from nbxmpp.structs import BlockingPush from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task from nbxmpp.types import BlockingReportValues if TYPE_CHECKING: from nbxmpp.client import Client class Blocking(BaseModule): def __init__(self, client: 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) -> Generator[set[JID] | Iq | None, Iq]: _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: set[JID] = set() for item in blocklist.getTags("item"): try: jid = JID.from_string(item.getAttr("jid")) except Exception: self._log.info("Invalid JID: %s", item.getAttr("jid")) continue blocked.add(jid) self._log.info("Received blocking list: %s", blocked) yield blocked @iq_request_task def block(self, jids: list[JID], report: BlockingReportValues | None = None): _task = yield response = yield _make_block_request(jids, report) yield process_response(response) @iq_request_task def unblock(self, jids: list[JID]): _task = yield response = yield _make_unblock_request(jids) yield process_response(response) @staticmethod def _process_blocking_push( client: Client, stanza: Iq, properties: BlockingProperties ) -> None: unblock = stanza.getTag("unblock", namespace=Namespace.BLOCKING) if unblock is not None: properties.blocking = _parse_push(unblock) 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 = Iq("get", Namespace.BLOCKING) iq.setQuery("blocklist") return iq def _make_block_request(jids: list[JID], report: BlockingReportValues | None) -> Iq: iq = Iq("set", Namespace.BLOCKING) query = iq.setQuery(name="block") for jid in jids: item = query.addChild(name="item", attrs={"jid": str(jid)}) if report in ("spam", "abuse"): action = item.addChild(name="report", namespace=Namespace.REPORTING) action.setTag(report) return iq def _make_unblock_request(jids: list[JID]) -> Iq: iq = Iq("set", Namespace.BLOCKING) query = iq.setQuery(name="unblock") for jid in jids: query.addChild(name="item", attrs={"jid": str(jid)}) return iq def _parse_push(node: Node) -> BlockingPush: items = node.getTags("item") if not items: return BlockingPush(block=set(), unblock=set(), unblock_all=True) jids: set[JID] = set() for item in items: jid = item.getAttr("jid") if not jid: continue try: jid = JID.from_string(jid) except Exception: continue jids.add(jid) block: set[JID] = set() unblock: set[JID] = set() if node.getName() == "block": block = jids else: unblock = jids return BlockingPush(block=block, unblock=unblock, unblock_all=False) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/bookmarks/000077500000000000000000000000001524361513300264205ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/bookmarks/__init__.py000066400000000000000000000000001524361513300305170ustar00rootroot00000000000000native_bookmarks.py000066400000000000000000000074301524361513300322550ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/bookmarks# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.bookmarks.util import build_conference_node from nbxmpp.modules.bookmarks.util import parse_bookmark from nbxmpp.modules.util import finalize from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import BookmarkData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client 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: Client) -> None: 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: Client, _stanza: Message, properties: MessageProperties ) -> None: 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: list[BookmarkData] = [] 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: 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: list[BookmarkData]): _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 pep_bookmarks.py000066400000000000000000000060571524361513300315570ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/bookmarks# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.bookmarks.util import build_storage_node from nbxmpp.modules.bookmarks.util import parse_bookmarks from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import BookmarkData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task BOOKMARK_OPTIONS = { "pubsub#persist_items": "true", "pubsub#access_model": "whitelist", } if TYPE_CHECKING: from nbxmpp.client import Client class PEPBookmarks(BaseModule): _depends = { "publish": "PubSub", "request_items": "PubSub", } def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: list[BookmarkData]): _task = yield self._log.info("Store Bookmarks") self.publish( Namespace.BOOKMARKS, build_storage_node(bookmarks), id_="current", options=BOOKMARK_OPTIONS, force_node_options=True, ) private_bookmarks.py000066400000000000000000000031031524361513300324320ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/bookmarks# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import StanzaError 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 from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.structs import BookmarkData from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class PrivateBookmarks(BaseModule): def __init__(self, client: Client) -> None: 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: list[BookmarkData]): _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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/bookmarks/util.py000066400000000000000000000111721524361513300277510ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later import logging from nbxmpp.errors import MalformedStanzaError from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import Node from nbxmpp.protocol import validate_resourcepart from nbxmpp.structs import BookmarkData from nbxmpp.util import from_xs_boolean from nbxmpp.util import to_xs_boolean def parse_nickname(nick: str | None) -> str | None: if nick is None: return None try: return validate_resourcepart(nick) except Exception: return None def parse_autojoin(autojoin: str | None) -> bool: if autojoin is None: return False try: return from_xs_boolean(autojoin) except ValueError: return False def parse_bookmark(item: Node) -> BookmarkData: 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 extensions = conference.getTag("extensions") return BookmarkData( jid=jid, name=name, nick=nick, autojoin=autojoin, password=password, extensions=extensions, ) def parse_bookmarks(item: Node, log: logging.Logger) -> list[BookmarkData]: 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: Node, log: logging.Logger) -> list[BookmarkData]: 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: Node, log: logging.Logger) -> list[BookmarkData]: bookmarks: list[BookmarkData] = [] 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: BookmarkData): 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) if bookmark.password: conference.setTagData("password", bookmark.password) if bookmark.extensions is not None: conference.addChild(node=bookmark.extensions) return conference def build_storage_node(bookmarks: list[BookmarkData]): 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 = Iq(typ="get") query = iq.addChild(name="query", namespace=Namespace.PRIVATE) query.addChild(name="storage", namespace=Namespace.BOOKMARKS) return iq python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/captcha.py000066400000000000000000000030761524361513300264130ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.modules.bits_of_binary import parse_bob_data from nbxmpp.modules.dataforms import extend_form from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.structs import CaptchaData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Captcha(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/chat_markers.py000066400000000000000000000031501524361513300274440ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.structs import ChatMarker from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class ChatMarkers(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/chatstates.py000066400000000000000000000031451524361513300271500ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import Chatstate from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Chatstates(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: try: chatstate = parse_chatstate(stanza) except ValueError as error: self._log.warning("Invalid chatstate: %s", error) self._log.warning(stanza) return if chatstate is None: return if properties.is_mam_message: return properties.chatstate = chatstate def parse_chatstate(stanza: Message) -> Chatstate | None: children = stanza.getChildren() for child in children: if child.getNamespace() == Namespace.CHATSTATES: return Chatstate(child.getName()) return None python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/correction.py000066400000000000000000000030051524361513300271470ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.structs import CorrectionData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Correction(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/dataforms.py000066400000000000000000000524211524361513300267660ustar00rootroot00000000000000# 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. # # SPDX-License-Identifier: GPL-3.0-or-later # XEP-0004: Data Forms from __future__ import annotations from typing import Any from typing import Literal from typing import Union from collections.abc import Iterator from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.simplexml import Node FieldT = Union[ "BooleanField", "StringField", "JidMultiField", "JidSingleField", "ListMultiField", "ListSingleField", "TextMultiField", ] # 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: Any, **kwargs: Any): if "extend" not in kwargs 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: str, **attrs: Any) -> FieldT: """Helper function to create a field of given type.""" field: FieldT = { "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: Node) -> FieldT: """ 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: Node) -> SimpleDataForm | MultipleDataForm: """ 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: str | None = None, var: str | None = None, value: str | None = None, label: str | None = None, desc: str | None = None, required: bool = False, options: list[tuple[str, str]] | None = None, extend: Node | None = None, ) -> 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 or [] @property def type_(self) -> str: """ 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: str) -> None: assert isinstance(value, str) self.setAttr("type", value) @property def var(self) -> str: """ Field identifier """ return self.getAttr("var") @var.setter def var(self, value: str) -> None: assert isinstance(value, str) self.setAttr("var", value) @var.deleter def var(self) -> None: self.delAttr("var") @property def label(self) -> str: """ Human-readable field name """ label_ = self.getAttr("label") if not label_: label_ = self.var return label_ @label.setter def label(self, value: str) -> None: assert isinstance(value, str) self.setAttr("label", value) @label.deleter def label(self) -> None: if self.getAttr("label"): self.delAttr("label") @property def description(self) -> str: """ Human-readable description of field meaning """ return self.getTagData("desc") or "" @description.setter def description(self, value: str) -> None: assert isinstance(value, str) if value == "": del self.description else: self.setTagData("desc", value) @description.deleter def description(self) -> None: desc = self.getTag("desc") if desc is not None: self.delChild(desc) @property def required(self) -> bool: """ Controls whether this field required to fill. Boolean """ return bool(self.getTag("required")) @required.setter def required(self, value: bool) -> None: 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 | None: """ Media data """ media = self.getTag("media", namespace=Namespace.DATA_MEDIA) if media: return Media(media) return None @media.setter def media(self, value: Media) -> None: del self.media self.addChild(node=value) @media.deleter def media(self) -> None: media = self.getTag("media") if media is not None: self.delChild(media) def is_valid(self) -> tuple[bool, str]: return True, "" class Uri(Node): def __init__(self, uri_tag: Node) -> None: Node.__init__(self, node=uri_tag) @property def type_(self) -> str | None: """ uri type """ return self.getAttr("type") @type_.setter def type_(self, value: str) -> None: self.setAttr("type", value) @type_.deleter def type_(self) -> None: self.delAttr("type") @property def uri_data(self) -> str: """ uri data """ return self.getData() @uri_data.setter def uri_data(self, value: str) -> None: self.setData(value) @uri_data.deleter def uri_data(self) -> None: self.setData(None) class Media(Node): def __init__(self, media_tag: Node) -> None: Node.__init__(self, node=media_tag) @property def uris(self) -> list[Uri]: """ URIs of the media element. """ return list(map(Uri, self.getTags("uri"))) @uris.setter def uris(self, value: list[Uri]) -> None: del self.uris for uri in value: self.addChild(node=uri) @uris.deleter def uris(self) -> None: for element in self.getTags("uri"): self.delChild(element) class BooleanField(DataField): @property def value(self) -> bool: """ 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: bool) -> None: self.setTagData("value", (value and "1") or "0") @value.deleter def value(self) -> None: 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) -> str: """ Value of field. May be any string """ return self.getTagData("value") or "" @value.setter def value(self, value: str) -> None: if value is None: # type: ignore value = "" self.setTagData("value", value) @value.deleter def value(self) -> None: try: self.delChild(self.getTag("value")) # type: ignore except ValueError: # if there already were no value tag pass def is_valid(self) -> tuple[bool, str]: 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) -> list[tuple[str, Any]]: """ Options """ options: list[tuple[str, Any]] = [] 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: list[tuple[Any, str]]) -> None: del self.options for value, label in values: self.addChild("option", {"label": label}).setTagData("value", value) @options.deleter def options(self) -> None: for element in self.getTags("option"): self.delChild(element) def iter_options(self) -> Iterator[tuple[Any, str]]: 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) -> tuple[bool, str]: 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) -> tuple[bool, str]: if self.value: try: JID.from_string(self.value) return True, "" except Exception as error: return False, str(error) if self.required: return False, "" return True, "" class ListMultiField(ListField): """ Covers list-multi fields """ @property def values(self) -> list[str]: """ Values held in field """ values: list[str] = [] for element in self.getTags("value"): values.append(element.getData()) return values @values.setter def values(self, values: list[Any]) -> None: del self.values for value in values: self.addChild("value").setData(value) @values.deleter def values(self) -> None: for element in self.getTags("value"): self.delChild(element) def iter_values(self) -> Iterator[str]: for element in self.getTags("value"): yield element.getData() def is_valid(self) -> tuple[bool, str]: 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) -> tuple[bool, str]: if self.values: for value in self.values: try: JID.from_string(value) except Exception as error: return False, str(error) return True, "" if self.required: return False, "" return True, "" class TextMultiField(DataField): @property def value(self) -> str: """ Value held in field """ value = "" for element in self.iterTags("value"): value += "\n" + element.getData() return value[1:] @value.setter def value(self, value: str) -> None: del self.value if value == "": return for line in value.split("\n"): self.addChild("value").setData(line) @value.deleter def value(self) -> None: for element in self.getTags("value"): self.delChild(element) def is_valid(self) -> tuple[bool, str]: 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: list[FieldT] | None = None, associated: SimpleDataForm | None = None, extend: Node | None = None, ) -> None: self.associated = associated self.vars: dict[str, FieldT] = {} 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[FieldT]: """ List of fields in this record """ return self.getTags("field") @fields.setter def fields(self, fields: list[FieldT]) -> None: 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) -> None: for element in self.getTags("field"): self.delChild(element) self.vars.clear() def iter_fields(self) -> Iterator[FieldT]: """ Iterate over fields in this record. Do not take associated into account """ yield from self.iterTags("field") def iter_with_associated(self) -> Iterator[tuple[Node, FieldT]]: """ 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: str) -> FieldT: return self.vars[item] def is_valid(self) -> bool: return all(field.is_valid()[0] for field in self.iter_fields()) def is_fake_form(self) -> bool: return bool(self.vars.get("fakeform", False)) class DataForm(ExtendedNode): def __init__( self, type_: str | None = None, title: str | None = None, instructions: str | None = None, extend: Node | None = None, ) -> 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) -> str | None: """ 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_: Literal["form", "submit", "cancel", "result"]) -> None: assert type_ in ("form", "submit", "cancel", "result") self.setAttr("type", type_) @property def title(self) -> str | None: """ Title of the form Human-readable, should not contain any \\r\\n. """ return self.getTagData("title") @title.setter def title(self, title: str) -> None: self.setTagData("title", title) @title.deleter def title(self) -> None: try: self.delChild("title") except ValueError: pass @property def instructions(self) -> str: """ 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: str) -> None: del self.instructions if value == "": return for line in value.split("\n"): self.addChild("instructions").setData(line) @instructions.deleter def instructions(self) -> None: for value in self.getTags("instructions"): self.delChild(value) @property def is_reported(self) -> bool: return self.getTag("reported") is not None class SimpleDataForm(DataForm, DataRecord): def __init__( self, type_: str | None = None, title: str | None = None, instructions: str | None = None, fields: list[FieldT] | None = None, extend: SimpleDataForm | Node | None = None, ) -> 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) -> SimpleDataForm: simple_form = SimpleDataForm(extend=self) del simple_form.title simple_form.instructions = "" to_be_removed: list[FieldT] = [] 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_: str | None = None, title: str | None = None, instructions: str | None = None, items: list[Node] | None = None, extend: Node | None = None, ) -> 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) -> list[Node]: """ A list of all records """ return list(self.iter_records()) @items.setter def items(self, records: list[DataRecord | Node]) -> None: del self.items for record in records: if not isinstance(record, DataRecord): DataRecord(extend=record) self.addChild(node=record) @items.deleter def items(self) -> None: for record in self.getTags("item"): self.delChild(record) def iter_records(self) -> Iterator[DataRecord | Node]: yield from self.getTags("item") python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/date_and_time.py000066400000000000000000000125301524361513300275600ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later import datetime as dt import logging import re import time log = logging.getLogger("nbxmpp.m.date_and_time") PATTERN_DATETIME = re.compile( r""" ([0-9]{4}-[0-9]{2}-[0-9]{2}) # Date T # Separator ([0-9]{2}:[0-9]{2}:[0-9]{2}) # Time (?P\.[0-9]{0,6})? # Fractual Seconds [0-9]* # lose everything > 6 (Z|[-+][0-9]{2}:[0-9]{2}) # UTC Offset $ # End of String """, re.VERBOSE, ) ZERO = dt.timedelta(0) HOUR = dt.timedelta(hours=1) SECOND = dt.timedelta(seconds=1) STDOFFSET = dt.timedelta(seconds=-time.timezone) if time.daylight: dstoffset = dt.timedelta(seconds=-time.altzone) else: dstoffset = STDOFFSET DSTOFFSET = dstoffset DSTDIFF = DSTOFFSET - STDOFFSET class LocalTimezone(dt.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_input: dt.datetime) -> dt.datetime: stamp = (dt_input - 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 dt.datetime( *args, microsecond=dt_input.microsecond, tzinfo=self, fold=fold ) def utcoffset(self, dt_input: dt.datetime) -> dt.timedelta: # type: ignore if self._isdst(dt_input): return DSTOFFSET return STDOFFSET def dst(self, dt_input: dt.datetime) -> dt.timedelta: # type: ignore if self._isdst(dt_input): return DSTDIFF return ZERO def tzname(self, _dt_input: dt.datetime) -> str: # type: ignore return "local" @staticmethod def _isdst(dt_input: dt.datetime) -> bool: tt = ( dt_input.year, dt_input.month, dt_input.day, dt_input.hour, dt_input.minute, dt_input.second, dt_input.weekday(), 0, 0, ) stamp = time.mktime(tt) tt = time.localtime(stamp) return tt.tm_isdst > 0 def create_tzinfo( hours: int = 0, minutes: int = 0, tz_string: str | None = None ) -> dt.timezone | None: if tz_string is None: return dt.timezone(dt.timedelta(hours=hours, minutes=minutes)) if tz_string.lower() == "z": return dt.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(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 dt.timezone(dt.timedelta(hours=hours, minutes=minutes)) def parse_datetime( timestring: str | None, check_utc: bool = False, convert: str | None = "utc", epoch: bool = False, ) -> dt.datetime | float | None: """ 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') match = PATTERN_DATETIME.match(timestring) if match is None: return None timestring = "".join(match.groups("")) strformat = "%Y-%m-%d%H:%M:%S%z" if match.group("frac"): # Fractional second addendum to Time strformat = "%Y-%m-%d%H:%M:%S.%f%z" try: date_time = dt.datetime.strptime(timestring, strformat) except ValueError: return None if not 1 < date_time.year < 9999: # Raise/Reduce MIN/MAX year so converting to different # timezones cannot get out of range return None if check_utc: if convert != "utc": raise ValueError('check_utc can only be used with convert="utc"') if date_time.tzinfo != dt.timezone.utc: return None if epoch: return date_time.timestamp() return date_time if convert == "utc": date_time = date_time.astimezone(dt.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 python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/delay.py000066400000000000000000000075411524361513300261070ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING import datetime as dt import logging from nbxmpp.modules.base import BaseModule from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Presence from nbxmpp.structs import MessageProperties from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client log = logging.getLogger("nbxmpp.m.delay") class Delay(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: # Determine if delay is from the server # Some servers use the bare jid, others the domain our_jid = self._client.get_bound_jid() jids = [our_jid.bare, our_jid.domain] if properties.from_muc: muc_jid = properties.jid jids += [muc_jid.bare, muc_jid.domain] 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 properties.user_timestamp = parse_delay(stanza, from_=jids) else: 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: Client, stanza: Presence, properties: PresenceProperties ) -> None: properties.user_timestamp = parse_delay(stanza) def parse_delay( stanza: Message | Presence, epoch: bool = True, convert: str = "utc", from_: list[str] | None = None, not_from: list[str] | None = None, ) -> dt.datetime | float | 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/delimiter.py000066400000000000000000000030241524361513300267570ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import Node from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Delimiter(BaseModule): def __init__(self, client: Client) -> None: 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: str): _task = yield response = yield _make_set_request(delimiter) yield process_response(response) def _make_request() -> Iq: node = Node("storage", attrs={"xmlns": Namespace.DELIMITER}) iq = Iq("get", Namespace.PRIVATE, payload=node) return iq def _make_set_request(delimiter: str) -> Iq: iq = Iq("set", Namespace.PRIVATE) roster = iq.getQuery().addChild("roster", namespace=Namespace.DELIMITER) roster.setData(delimiter) return iq python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/discovery.py000066400000000000000000000104571524361513300270200ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING import logging import time from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.dataforms import extend_form from nbxmpp.modules.dataforms import MultipleDataForm from nbxmpp.modules.dataforms import SimpleDataForm from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_ITEM_NOT_FOUND from nbxmpp.protocol import ErrorNode from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import DiscoIdentity from nbxmpp.structs import DiscoInfo from nbxmpp.structs import DiscoItem from nbxmpp.structs import DiscoItems from nbxmpp.structs import IqProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client log = logging.getLogger("nbxmpp.m.discovery") class Discovery(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Iq, _properties: IqProperties ) -> None: 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: JID | str, node: str | None = None): _task = yield 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: JID | str, node: str | None = None): _task = yield 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: Iq, timestamp: float | None = None) -> DiscoInfo: identities: list[DiscoIdentity] = [] features: list[str] = [] dataforms: list[SimpleDataForm | MultipleDataForm] = [] if timestamp is None: timestamp = time.time() query = stanza.getQuery() for node in query.getTags("identity"): attrs = node.getAttrs() try: identities.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=identities, features=features, dataforms=dataforms, timestamp=timestamp, ) def parse_disco_items(stanza: Iq) -> DiscoItems: items: list[DiscoItem] = [] 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: str, jid: str, node: str | None = None) -> Iq: iq = Iq("get", to=jid, queryNS=namespace) if node: iq.getQuery().setAttr("node", node) return iq python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/eme.py000066400000000000000000000026401524361513300255520ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.structs import EMEData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class EME(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: encryption = stanza.getTag("encryption", namespace=Namespace.EME) if encryption is None: return name = encryption.getAttr("name") or "" 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/entity_caps.py000066400000000000000000000073021524361513300273260ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Presence from nbxmpp.structs import DiscoIdentity from nbxmpp.structs import DiscoInfo from nbxmpp.structs import EntityCapsData from nbxmpp.structs import IqProperties from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler from nbxmpp.util import compute_caps_hash if TYPE_CHECKING: from nbxmpp.client import Client class EntityCaps(BaseModule): def __init__(self, client: Client) -> None: 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: str | None = None self._node: str | None = None self._caps: DiscoInfo | None = None self._caps_hash: str | None = None def _process_disco_info( self, client: Client, stanza: Iq, _properties: IqProperties ) -> None: 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: Client, stanza: Presence, properties: PresenceProperties ) -> None: 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) -> EntityCapsData | None: if self._caps is None: return None assert self._uri is not None assert self._caps_hash is not None return EntityCapsData(hash="sha-1", node=self._uri, ver=self._caps_hash) def set_caps( self, identities: list[DiscoIdentity], features: list[str], uri: str ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/entity_time.py000066400000000000000000000101141524361513300273310ustar00rootroot00000000000000# Copyright (C) 2021 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING import datetime as dt from collections.abc import Callable from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.date_and_time import create_tzinfo from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_FORBIDDEN from nbxmpp.protocol import ERR_SERVICE_UNAVAILABLE from nbxmpp.protocol import Error from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import IqProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class EntityTime(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="iq", callback=self._answer_request, priority=60, typ="get", ns=Namespace.TIME, ), ] self._enabled = False self._allow_reply_func: Callable[..., bool] | None = None def disable(self) -> None: self._enabled = False def enable(self) -> None: self._enabled = True def set_allow_reply_func(self, func: Callable[..., bool]) -> None: self._allow_reply_func = func @iq_request_task def request_entity_time(self, jid: JID): _task = yield response = yield _make_request(jid) if response.isError(): raise StanzaError(response) yield _parse_response(response) def _answer_request( self, _client: Client, stanza: Iq, _properties: IqProperties ) -> None: self._log.info("Request received from %s", stanza.getFrom()) if not self._enabled: self._client.send_stanza(Error(stanza, ERR_SERVICE_UNAVAILABLE)) raise NodeProcessed if self._allow_reply_func is not None: if not self._allow_reply_func(stanza.getFrom()): self._client.send_stanza(Error(stanza, ERR_FORBIDDEN)) raise NodeProcessed time, tzo = get_local_time() iq = stanza.buildSimpleReply("result") time_node = iq.addChild("time", namespace=Namespace.TIME) time_node.setTagData("utc", time) time_node.setTagData("tzo", tzo) self._log.info("Send time: %s %s", time, tzo) self._client.send_stanza(iq) raise NodeProcessed def get_local_time() -> tuple[str, str]: utc = dt.datetime.now(tz=dt.timezone.utc) utc_formated = utc.strftime("%Y-%m-%dT%H:%M:%SZ") local = utc.astimezone() if local.utcoffset() == utc.utcoffset(): offset = "Z" else: # %z has the format ±HHMM[SS[.ffffff]] offset = local.strftime("%z")[:5] offset = f"{offset[:-2]}:{offset[-2:]}" return utc_formated, offset def _make_request(jid: JID) -> Iq: iq = Iq("get", to=jid) iq.addChild("time", namespace=Namespace.TIME) return iq def _parse_response(response: Iq) -> str: time_ = response.getTag("time") if not time_: raise MalformedStanzaError("time node missing", response) tzo = time_.getTagData("tzo") if not tzo: raise MalformedStanzaError("tzo node or data missing", response) remote_tz = create_tzinfo(tz_string=tzo) if remote_tz is None: raise MalformedStanzaError("invalid tzo data", response) utc_time = time_.getTagData("utc") if not utc_time: raise MalformedStanzaError("utc node or data missing", response) date_time = parse_datetime(utc_time, check_utc=True) if date_time is None: raise MalformedStanzaError("invalid timezone definition", response) date_time = date_time.astimezone(remote_tz) return date_time.strftime("%c %Z") python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/fallback.py000066400000000000000000000070421524361513300265440ustar00rootroot00000000000000from __future__ import annotations from typing import Any import logging import operator from dataclasses import dataclass from nbxmpp.exceptions import FallbackLanguageError from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message @dataclass(frozen=True) class FallbackRange: start: int end: int def __post_init__(self): if self.start < 0 or self.end < 0: raise ValueError("Range with negative numbers: %s", self) if self.start >= self.end: raise ValueError("Range with start > end: %s", self) FallbackLangMapT = dict[str | None, FallbackRange | None] FallbacksForT = dict[str, FallbackLangMapT | None] def parse_fallback_indication( log: logging.Logger | logging.LoggerAdapter[Any], stanza: Message, ) -> FallbacksForT | None: fallbacks = stanza.getTags( "fallback", namespace=Namespace.FALLBACK, ) if not fallbacks: return None fallbacks_for: FallbacksForT = {} for fallback in fallbacks: for_ = fallback.getAttr("for") if not for_: log.warning('Missing "for" attribute on fallback indication') return bodies = fallback.getTags("body") if not bodies: # No means fallback is for all bodies fallbacks_for[for_] = None continue fallback_lang_map: FallbackLangMapT = {} for body in bodies: lang = body.getAttr("xml:lang") or None start = body.getAttr("start") or None end = body.getAttr("end") or None if type(start) is not type(end): log.warning("Incorrect range on fallback indication") return range_ = None if start is not None: assert end is not None try: range_ = FallbackRange(start=int(start), end=int(end)) except Exception as error: log.warning("Incorrect range on fallback indication: %s", error) return fallback_lang_map[lang] = range_ if fallback_lang_map: # Only store data for the key if there was at least one # valid body fallbacks_for[for_] = fallback_lang_map return fallbacks_for def strip_fallback( fallbacks_for: FallbacksForT, fallback_ns: set[str], lang: str | None, text: str, ) -> str: fallbacks: list[FallbackRange] = [] # Gather all fallbacks we support for ns in fallback_ns: try: fallback_lang_map = fallbacks_for[ns] except KeyError: continue if fallback_lang_map is None: # At least one of the fallbacks declared the whole # body as fallback return "" try: range_ = fallback_lang_map[lang] except KeyError: # Fallback missing for this language, assume the stanza is # malformed and return the whole text raise FallbackLanguageError if range_ is None: # No range means the whole body is considered a fallback return "" if range_.end > len(text): # Abort processing, because range is invalid return text fallbacks.append(range_) if not fallbacks: return text fallbacks.sort(key=operator.attrgetter("start"), reverse=True) stripped_text = text for range_ in fallbacks: stripped_text = stripped_text[: range_.start] + stripped_text[range_.end :] return stripped_text python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/http_auth.py000066400000000000000000000033761524361513300270130ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import Message from nbxmpp.structs import HTTPAuthData from nbxmpp.structs import IqProperties from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class HTTPAuth(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Iq | Message, properties: IqProperties | MessageProperties, ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/http_upload.py000066400000000000000000000046001524361513300273250ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import HTTPUploadStanzaError from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.structs import HTTPUploadData from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client ALLOWED_HEADERS = ["Authorization", "Cookie", "Expires"] class HTTPUpload(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [] @iq_request_task def request_slot(self, jid: JID, filename: str, size: int, content_type: str): _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: dict[str, str] = {} 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: JID, filename: str, size: int, content_type: str) -> Iq: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/ibb.py000066400000000000000000000130321524361513300255350ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_BAD_REQUEST from nbxmpp.protocol import ERR_FEATURE_NOT_IMPLEMENTED from nbxmpp.protocol import Error as ErrorStanza from nbxmpp.protocol import Iq from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import IBBData from nbxmpp.structs import IqProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task from nbxmpp.util import b64decode from nbxmpp.util import b64encode if TYPE_CHECKING: from nbxmpp.client import Client class IBB(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Iq, properties: IqProperties ) -> None: 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: Iq, open_: Node) -> IBBData: 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: Iq, close: Node) -> IBBData: 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: Iq, data: Node) -> IBBData: 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()) 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: Iq, error: str | None = None) -> 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: str, sid: str, block_size: int): _task = yield response = yield _make_ibb_open(jid, sid, block_size) yield process_response(response) @iq_request_task def send_close(self, jid: str, sid: str): _task = yield response = yield _make_ibb_close(jid, sid) yield process_response(response) @iq_request_task def send_data(self, jid: str, sid: str, seq: int, data: bytes): _task = yield response = yield _make_ibb_data(jid, sid, seq, data) yield process_response(response) def _make_ibb_open(jid: str, sid: str, block_size: int) -> Iq: 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: str, sid: str) -> Iq: iq = Iq("set", to=jid) iq.addChild("close", {"sid": sid}, namespace=Namespace.IBB) return iq def _make_ibb_data(jid: str, sid: str, seq: int, data: bytes) -> Iq: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/idle.py000066400000000000000000000030451524361513300257210ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Presence from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Idle(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Presence, properties: PresenceProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/iq.py000066400000000000000000000032721524361513300254170ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import IqType from nbxmpp.modules.base import BaseModule from nbxmpp.protocol import ERR_BAD_REQUEST from nbxmpp.protocol import Error as ErrorStanza from nbxmpp.protocol import Iq from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import IqProperties from nbxmpp.structs import StanzaHandler from nbxmpp.util import error_factory if TYPE_CHECKING: from nbxmpp.client import Client class BaseIq(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Iq, properties: IqProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/last_activity.py000066400000000000000000000060551524361513300276670ustar00rootroot00000000000000# Copyright (C) 2021 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from collections.abc import Callable from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_FORBIDDEN from nbxmpp.protocol import ERR_SERVICE_UNAVAILABLE from nbxmpp.protocol import Error from nbxmpp.protocol import Iq from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import IqProperties from nbxmpp.structs import LastActivityData from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class LastActivity(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="iq", callback=self._answer_request, priority=60, typ="get", ns=Namespace.LAST, ), ] self._idle_func: Callable[..., int] | None = None self._allow_reply_func: Callable[..., bool] | None = None def disable(self) -> None: self._idle_func = None def set_idle_func(self, func: Callable[..., int]) -> None: self._idle_func = func def set_allow_reply_func(self, func: Callable[..., bool]) -> None: self._allow_reply_func = func @iq_request_task def request_last_activity(self, jid: str): _task = yield response = yield _make_request(jid) if response.isError(): raise StanzaError(response) yield _parse_response(response) def _answer_request( self, _client: Client, stanza: Iq, _properties: IqProperties ) -> None: self._log.info("Request received from %s", stanza.getFrom()) if self._idle_func is None: self._client.send_stanza(Error(stanza, ERR_SERVICE_UNAVAILABLE)) raise NodeProcessed if self._allow_reply_func is not None: if not self._allow_reply_func(stanza.getFrom()): self._client.send_stanza(Error(stanza, ERR_FORBIDDEN)) raise NodeProcessed seconds = self._idle_func() iq = stanza.buildReply("result") query = iq.getQuery() query.setAttr("seconds", seconds) self._log.info("Send last activity: %s", seconds) self._client.send_stanza(iq) raise NodeProcessed def _make_request(jid: str) -> Iq: return Iq("get", queryNS=Namespace.LAST, to=jid) def _parse_response(response: Iq) -> LastActivityData: query = response.getQuery() seconds = query.getAttr("seconds") try: seconds = int(seconds) except Exception: raise MalformedStanzaError("seconds attribute invalid", response) return LastActivityData(seconds=seconds, status=query.getData()) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/location.py000066400000000000000000000050021524361513300266070ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import LOCATION_DATA from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.structs import LocationData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Location(BaseModule): _depends = {"publish": "PubSub"} def __init__(self, client: Client) -> None: 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: Client, _stanza: Message, properties: MessageProperties ) -> None: 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: LocationData | None): 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/mam.py000066400000000000000000000136761524361513300255710ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING import datetime as dt from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.dataforms import create_field from nbxmpp.modules.dataforms import SimpleDataForm from nbxmpp.modules.rsm import parse_rsm from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import Node from nbxmpp.structs import MAMPreferencesData from nbxmpp.structs import MAMQueryData from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class MAM(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [] @iq_request_task def make_query( self, jid: JID, queryid: str | None = None, start: dt.datetime | None = None, end: dt.datetime | None = None, with_: str | None = None, after: str | None = None, max_: int = 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: str | None, always: list[str], never: list[str]): _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: dt.datetime | None, end: dt.datetime | None, with_: str | None ) -> SimpleDataForm: 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_: int | None, after: str | None) -> Node: 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: JID, queryid: str | None, start: dt.datetime | None, end: dt.datetime | None, with_: str | None, after: str | None, max_: int | None, ) -> Iq: 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 = Iq("get", queryNS=Namespace.MAM_2) iq.setQuery("prefs") return iq def _get_preference_jids(node: Iq) -> list[JID]: jids: list[JID] = [] 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: str, always: list[str], never: list[str]) -> Iq: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/mds.py000066400000000000000000000103701524361513300255660ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import MDSData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client MDS_OPTIONS = { "pubsub#persist_items": "true", "pubsub#max_items": "max", "pubsub#send_last_published_item": "never", "pubsub#access_model": "whitelist", } class MDS(BaseModule): """ XEP-0490 """ _depends = {"publish": "PubSub", "request_items": "PubSub"} def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="message", callback=self._process_pubsub_mds, ns=Namespace.PUBSUB_EVENT, priority=16, ), ] def _process_pubsub_mds( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: if not properties.is_pubsub_event: return if properties.pubsub_event.node != Namespace.MDS: return item = properties.pubsub_event.item if item is None: return try: data = self._parse_item(item) except MalformedStanzaError as error: self._log.warning(error) self._log.warning(stanza) raise NodeProcessed pubsub_event = properties.pubsub_event._replace(data=data) self._log.info("Received MDS: %s", data) properties.pubsub_event = pubsub_event @iq_request_task def set_mds(self, jid: JID, stanza_id: str, by: JID | None = None): task = yield if by is None: own_jid = self._client.get_bound_jid() assert own_jid is not None by = own_jid.new_as_bare() displayed = Node("displayed", {"xmlns": Namespace.MDS}) displayed.addChild( "stanza-id", namespace=Namespace.SID, attrs={"id": stanza_id, "by": str(by)} ) result = yield self.publish( Namespace.MDS, displayed, id_=str(jid), options=MDS_OPTIONS, force_node_options=True, ) yield finalize(task, result) @iq_request_task def get_mds(self): task = yield items = yield self.request_items(Namespace.MDS) raise_if_error(items) if not items: yield task.set_result(None) data: list[MDSData] = [] for item in items: try: data.append(self._parse_item(item)) except MalformedStanzaError as error: self._log.warning(error) self._log.warning(item) yield data def _parse_item(self, item: Node) -> MDSData: item_id = item.getAttr("id") try: jid = JID.from_string(item_id) except Exception as e: raise MalformedStanzaError( f'MDS item ID "{item_id}" is not a valid JID: {e}', item ) displayed = item.getTag("displayed", namespace=Namespace.MDS) if not displayed: raise MalformedStanzaError("Bad MDS event (no displayed tag)", item) sid = displayed.getTag("stanza-id", namespace=Namespace.SID) if sid is None: raise MalformedStanzaError("Bad MDS event (no stanza-id tag)", item) stanza_id = sid.getAttr("id") if not stanza_id: raise MalformedStanzaError("Bad MDS event (no stanza-id ID)", item) by = sid.getAttr("by") try: stanza_id_by = JID.from_string(by) except Exception as e: raise MalformedStanzaError( f'MDS stanza-id by "{by}" is not a valid JID: {e}', item ) return MDSData(jid=jid, stanza_id=stanza_id, stanza_id_by=stanza_id_by) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/message.py000066400000000000000000000121411524361513300264250ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import MessageType from nbxmpp.modules.base import BaseModule from nbxmpp.modules.fallback import parse_fallback_indication from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import BodyData from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.structs import StanzaIDData from nbxmpp.structs import XHTMLData from nbxmpp.util import error_factory if TYPE_CHECKING: from nbxmpp.client import Client class BaseMessage(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: properties.type = self._parse_type(stanza) 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() self._parse_if_private_message(stanza, properties) properties.remote_jid = self._determine_remote_jid(properties) properties.from_ = stanza.getFrom() properties.to = stanza.getTo() properties.id = stanza.getID() properties.self_message = self._parse_self_message(stanza, properties) properties.origin_id = stanza.getOriginID() properties.stanza_ids = self._parse_stanza_ids(stanza) if properties.type.is_error: properties.error = error_factory(stanza) def _determine_remote_jid(self, properties: MessageProperties) -> JID | None: if properties.is_muc_pm: return properties.jid return properties.jid.new_as_bare() def _parse_if_private_message( self, stanza: Message, properties: MessageProperties ) -> None: muc_user = stanza.getTag("x", namespace=Namespace.MUC_USER) if muc_user is None: return if not properties.jid.is_full: return if properties.type.is_chat or ( properties.type.is_error and not muc_user.getChildren() ): properties.muc_private_message = True def _process_message_after_base( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: # This handler runs after decryption, when handlers had the chance # to decrypt the body fallbacks_for = parse_fallback_indication(self._log, stanza) properties.body = stanza.getBody() properties.bodies = BodyData( stanza, fallbacks_for, self._client.get_supported_fallback_ns() ) 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: Message) -> MessageType: 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: Message, properties: MessageProperties) -> bool: if properties.type.is_groupchat: return False return stanza.getFrom().bare_match(stanza.getTo()) def _parse_stanza_ids(self, stanza: Message) -> list[StanzaIDData]: stanza_ids: list[StanzaIDData] = [] for stanza_id in stanza.getTags("stanza-id", namespace=Namespace.SID): id_ = stanza_id.getAttr("id") by = stanza_id.getAttr("by") if not id_ or not by: self._log.warning("Missing attributes on stanza-id") self._log.warning(stanza) continue stanza_ids.append(StanzaIDData(id=id_, by=by)) return stanza_ids python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/misc.py000066400000000000000000000076251524361513300257470ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later import logging from nbxmpp.modules.delay import parse_delay from nbxmpp.namespaces import Namespace from nbxmpp.protocol import InvalidFrom from nbxmpp.protocol import InvalidStanza from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Protocol from nbxmpp.structs import CarbonData from nbxmpp.structs import MAMData log = logging.getLogger("nbxmpp.m.misc") def unwrap_carbon( stanza: Protocol, own_jid: JID ) -> tuple[Message | Protocol, CarbonData | None]: 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: Protocol, own_jid: JID ) -> tuple[Message | Protocol, MAMData | None]: 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, epoch=True) 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: str, xmllang: str | None = None) -> str | 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/mood.py000066400000000000000000000053451524361513300257470ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import MOODS from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import MessageProperties from nbxmpp.structs import MoodData from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Mood(BaseModule): _depends = {"publish": "PubSub"} def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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.getChildren(): 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: MoodData): 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/muc/000077500000000000000000000000001524361513300252145ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/muc/__init__.py000066400000000000000000000000431524361513300273220ustar00rootroot00000000000000from .muc import MUC # noqa: F401 python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/muc/hats.py000066400000000000000000000041421524361513300265260ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Presence from nbxmpp.structs import Hat from nbxmpp.structs import HatData from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Hats(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="presence", callback=self._process_hats, ns=Namespace.HATS, priority=15, ), StanzaHandler( name="presence", callback=self._process_hats, ns=Namespace.HATS_LEGACY, priority=15, ), ] def _process_hats( self, _client: Client, stanza: Presence, properties: PresenceProperties, ) -> None: hats = stanza.getTag("hats", namespace=Namespace.HATS) if hats is None: hats = stanza.getTag("hats", namespace=Namespace.HATS_LEGACY) if hats is None: return hat_data = HatData() for hat in hats.getTags("hat"): uri = hat.getAttr("uri") title = hat.getAttr("title") if not uri or not title: self._log.warning("Invalid hat received") self._log.warning(stanza) raise NodeProcessed hue_str = hat.getAttr("hue") if hue_str is None: hue: float | None = None else: try: hue = float(hue_str) except ValueError: self._log.warning("Invalid hue value: %s", hue_str) hue = None lang = hat.getAttr("xml:lang") hat_data.add_hat(Hat(uri, title, hue), lang) properties.hats = hat_data python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/muc/moderation.py000066400000000000000000000201101524361513300277210ustar00rootroot00000000000000# This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later # XEP-0425: Message Moderation from __future__ import annotations from typing import TYPE_CHECKING import datetime as dt from nbxmpp import JID from nbxmpp.modules.base import BaseModule from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.simplexml import Node from nbxmpp.structs import MessageProperties from nbxmpp.structs import ModerationData from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Moderation(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="message", callback=self._process_moderation_0_message, typ="groupchat", ns=Namespace.FASTEN, priority=20, ), StanzaHandler( name="message", callback=self._process_moderation_0_tombstone_message, typ="groupchat", ns=Namespace.MESSAGE_MODERATE, priority=20, ), StanzaHandler( name="message", callback=self._process_moderation_1_message, typ="groupchat", ns=Namespace.MESSAGE_RETRACT_1, priority=20, ), ] @iq_request_task def send_moderation_request( self, namespace: str, muc_jid: JID, stanza_id: str, reason: str | None = None, ): _task = yield if namespace == Namespace.MESSAGE_MODERATE: response = yield _make_moderation_request_0(muc_jid, stanza_id, reason) else: response = yield _make_moderation_request_1(muc_jid, stanza_id, reason) yield process_response(response) def _process_moderation_1_message( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: retract, is_tombstone = _get_retract_element(stanza, properties) if retract is None: return stamp = _parse_moderation_timestamp(retract, is_tombstone, properties) moderated = retract.getTag("moderated", namespace=Namespace.MESSAGE_MODERATE_1) if moderated is None: return try: by = _parse_by_attr(moderated) except ValueError as error: self._log.warning("Unable to determine by attribute: %s", error) by = None occupant_id = moderated.getTagAttr( "occupant-id", "id", namespace=Namespace.OCCUPANT_ID ) if is_tombstone: stanza_id = properties.mam.id else: stanza_id = retract.getAttr("id") if stanza_id is None: self._log.warning("Unable to determine stanza-id") self._log.warning(stanza) raise NodeProcessed properties.moderation = ModerationData( stanza_id=stanza_id, by=by, reason=retract.getTagData("reason"), stamp=stamp, is_tombstone=is_tombstone, occupant_id=occupant_id, ) def _process_moderation_0_tombstone_message( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: if not properties.is_mam_message: return moderated = stanza.getTag("moderated", namespace=Namespace.MESSAGE_MODERATE) if moderated is None: return properties.moderation = self._parse_moderated_0( moderated, properties, properties.mam.id ) def _process_moderation_0_message( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: if not properties.jid.is_bare: return apply_to = stanza.getTag("apply-to", namespace=Namespace.FASTEN) if apply_to is None: return moderated = apply_to.getTag("moderated", namespace=Namespace.MESSAGE_MODERATE) if moderated is None: return stanza_id = apply_to.getAttr("id") if stanza_id is None: self._log.warning("apply-to element without stanza-id") self._log.warning(stanza) raise NodeProcessed properties.moderation = self._parse_moderated_0( moderated, properties, stanza_id ) def _parse_moderated_0( self, moderated: Node, properties: MessageProperties, stanza_id: str ) -> ModerationData | None: retract, is_tombstone = _get_retract_element(moderated, properties) if retract is None: self._log.warning("Failed to find element") return None try: by = _parse_by_attr(moderated) except ValueError as error: self._log.warning(error) by = None stamp = _parse_moderation_timestamp(retract, is_tombstone, properties) occupant_id = moderated.getTagAttr( "occupant-id", "id", namespace=Namespace.OCCUPANT_ID ) return ModerationData( stanza_id=stanza_id, by=by, occupant_id=occupant_id, reason=moderated.getTagData("reason"), stamp=stamp, is_tombstone=is_tombstone, ) def _get_retract_element( element: Node, properties: MessageProperties ) -> tuple[Node | None, bool]: """ returns a tuple Node and a boolean value which signals if it is a tombstone """ retract = element.getTag("retract", namespace=Namespace.MESSAGE_RETRACT) if retract is not None: return retract, False retract = element.getTag("retract", namespace=Namespace.MESSAGE_RETRACT_1) if retract is not None: return retract, False if not properties.is_mam_message: return None, False retracted = element.getTag("retracted", namespace=Namespace.MESSAGE_RETRACT) if retracted is not None: return retracted, True retracted = element.getTag("retracted", namespace=Namespace.MESSAGE_RETRACT_1) if retracted is not None: return retracted, True return None, False def _parse_by_attr(moderated: Node) -> JID | None: by_attr = moderated.getAttr("by") if by_attr is None: return None try: return JID.from_string(by_attr) except Exception as error: raise ValueError("Invalid JID: %s, %s" % (by_attr, error)) def _parse_moderation_timestamp( retract: Node, is_tombstone: bool, properties: MessageProperties ) -> dt.datetime: if is_tombstone: stamp_attr = retract.getAttr("stamp") stamp = parse_datetime(stamp_attr, check_utc=True, convert="utc") if stamp is not None: assert isinstance(stamp, dt.datetime) return stamp if properties.is_mam_message: stamp = properties.mam.timestamp else: stamp = properties.timestamp return dt.datetime.fromtimestamp(stamp, dt.timezone.utc) def _make_moderation_request_0(muc_jid: JID, stanza_id: str, reason: str | None) -> Iq: iq = Iq("set", Namespace.FASTEN, to=muc_jid) query = iq.setQuery(name="apply-to") query.setAttr("id", stanza_id) moderate = query.addChild(name="moderate", namespace=Namespace.MESSAGE_MODERATE) moderate.addChild(name="retract", namespace=Namespace.MESSAGE_RETRACT) if reason is not None: moderate.addChild(name="reason", payload=[reason]) return iq def _make_moderation_request_1(muc_jid: JID, stanza_id: str, reason: str | None) -> Iq: iq = Iq("set", Namespace.MESSAGE_MODERATE_1, to=muc_jid) query = iq.setQuery(name="moderate") query.setAttr("id", stanza_id) query.addChild(name="retract", namespace=Namespace.MESSAGE_RETRACT_1) if reason is not None: query.addChild(name="reason", payload=[reason]) return iq python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/muc/muc.py000066400000000000000000000516461524361513300263660ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Literal from typing import TYPE_CHECKING from nbxmpp.const import InviteType from nbxmpp.const import MessageType from nbxmpp.const import StatusCode from nbxmpp.errors import is_error from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.dataforms import extend_form from nbxmpp.modules.muc.util import build_direct_invite from nbxmpp.modules.muc.util import build_mediated_invite from nbxmpp.modules.muc.util import make_affiliation_request from nbxmpp.modules.muc.util import make_cancel_config_request from nbxmpp.modules.muc.util import make_captcha_request from nbxmpp.modules.muc.util import make_config_request from nbxmpp.modules.muc.util import make_destroy_request from nbxmpp.modules.muc.util import make_set_affiliation_request from nbxmpp.modules.muc.util import make_set_config_request from nbxmpp.modules.muc.util import make_set_role_request from nbxmpp.modules.muc.util import MucInfoResult from nbxmpp.modules.muc.util import parse_muc_user from nbxmpp.modules.util import parse_xmpp_uri from nbxmpp.modules.util import process_response from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import DataField from nbxmpp.protocol import DataForm from nbxmpp.protocol import ERR_NOT_ACCEPTABLE from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Presence from nbxmpp.protocol import StanzaMalformed from nbxmpp.structs import AffiliationResult from nbxmpp.structs import DeclineData from nbxmpp.structs import InviteData from nbxmpp.structs import MessageProperties from nbxmpp.structs import MucConfigResult from nbxmpp.structs import MucDestroyed from nbxmpp.structs import MucSubject from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler from nbxmpp.structs import VoiceRequest from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client AffiliationT = Literal["owner", "admin", "member", "outcast", "none"] RoleT = Literal["moderator", "participant", "visitor", "none"] class MUC(BaseModule): _depends = { "disco_info": "Discovery", "request_vcard": "VCardTemp", "send_moderation_request": "Moderation", } def __init__(self, client: Client) -> None: 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_message_base, ns=Namespace.MUC_USER, priority=5, ), StanzaHandler( name="message", callback=self._process_message_before_decryption, typ="groupchat", priority=6, ), StanzaHandler( name="message", callback=self._process_message_after_decryption, typ="groupchat", priority=16, ), 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, typ="normal", ns=Namespace.DATA, priority=11, ), StanzaHandler( name="message", callback=self._process_message, ns=Namespace.MUC_USER, priority=13, ), ] @staticmethod def _process_muc_presence( _client: Client, stanza: Presence, properties: PresenceProperties ) -> None: 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 occupant_id = stanza.getTagAttr( "occupant-id", "id", namespace=Namespace.OCCUPANT_ID ) properties.occupant_id = occupant_id def _process_muc_user_presence( self, _client: Client, stanza: Presence, properties: PresenceProperties ) -> None: 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() occupant_id = stanza.getTagAttr( "occupant-id", "id", namespace=Namespace.OCCUPANT_ID ) properties.occupant_id = occupant_id 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[StatusCode] = 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_message_before_decryption( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: properties.from_muc = True properties.muc_jid = properties.jid.new_as_bare() properties.muc_nickname = properties.jid.resource occupant_id = stanza.getTagAttr( "occupant-id", "id", namespace=Namespace.OCCUPANT_ID ) properties.occupant_id = occupant_id 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_after_decryption( self, _client: Client, _stanza: Message, properties: MessageProperties ) -> None: if properties.subject is None: return if properties.body is not None or properties.thread is not None: return properties.muc_subject = MucSubject( text=properties.subject, author=properties.muc_nickname, timestamp=properties.user_timestamp, ) def _process_message_base( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: muc_user = stanza.getTag("x", namespace=Namespace.MUC_USER) if muc_user is None: return 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 def _process_message( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: muc_user = stanza.getTag("x", namespace=Namespace.MUC_USER) if muc_user is None: return occupant_id = stanza.getTagAttr( "occupant-id", "id", namespace=Namespace.OCCUPANT_ID ) properties.occupant_id = occupant_id 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[StatusCode] = 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: JID, voice_request: VoiceRequest) -> str: form = voice_request.form form.type_ = "submit" form["muc#request_allow"].value = True return self._client.send_stanza(Message(to=muc_jid, payload=form)) @iq_request_task def get_affiliation(self, jid: JID | str, affiliation: AffiliationT): _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: user_jid = item.getAttr("jid") if user_jid is None: self._log.warning("Received an affiliation item without JID: %s", item) self._log.warning(response) continue try: jid = JID.from_string(user_jid) except Exception as e: self._log.warning("'%s' is an invalid JID: %s", user_jid, e) 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: JID, reason: str | None = None, jid: str | None = None): _task = yield response = yield make_destroy_request(room_jid, reason, jid) yield process_response(response) @iq_request_task def request_info( self, jid: JID | str, request_vcard: bool = True, allow_redirect: bool = 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: 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: JID): _task = yield response = yield make_config_request(room_jid) if response.isError(): raise StanzaError(response) jid = response.getFrom() children = response.getQueryChildren() for form in children: 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: JID): _task = yield response = yield make_cancel_config_request(room_jid) yield process_response(response) @iq_request_task def set_affiliation(self, room_jid: JID, users_dict: dict[JID, dict[str, str]]): _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: JID, nick: str, role: RoleT, reason: str | None = None ): _task = yield response = yield make_set_role_request(room_jid, nick, role, reason) yield process_response(response) def moderate_message( self, namespace: str, room_jid: JID, stanza_id: str, reason: str | None = None ) -> None: self.send_moderation_request(namespace, room_jid, stanza_id, reason) def set_subject(self, room_jid: JID, subject: MucSubject | None) -> str: message = Message(room_jid, typ="groupchat", subject=subject) self._log.info("Set subject for %s", room_jid) return self._client.send_stanza(message) def decline(self, room: JID | str, to: JID | str, reason: str | None = None) -> str: 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) return self._client.send_stanza(message) def request_voice(self, room: JID) -> str: 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) return self._client.send_stanza(message) def invite( self, room: JID, to: JID, password: str | None, reason: str | None = None, continue_: bool = False, type_: InviteType = InviteType.MEDIATED, ) -> str: 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: JID, form_node): _task = yield response = yield make_captcha_request(room_jid, form_node) yield process_response(response) def cancel_captcha(self, room_jid: JID, message_id: str) -> None: message = Message(typ="error", to=room_jid) message.setID(message_id) message.setError(ERR_NOT_ACCEPTABLE) self._client.send_stanza(message) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/muc/util.py000066400000000000000000000126631524361513300265530ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Literal from dataclasses import dataclass from nbxmpp.const import Affiliation from nbxmpp.const import Role from nbxmpp.modules.vcard_temp import VCard from nbxmpp.namespaces import Namespace from nbxmpp.protocol import InvalidJid from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import StanzaMalformed from nbxmpp.simplexml import Node from nbxmpp.structs import DiscoInfo from nbxmpp.structs import MucUserData AffiliationT = Literal["owner", "admin", "member", "outcast", "none"] RoleT = Literal["moderator", "participant", "visitor", "none"] @dataclass class MucInfoResult: info: DiscoInfo vcard: VCard | None = None redirected: bool = False def make_affiliation_request(jid: JID | str, affiliation: AffiliationT) -> Iq: 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: JID, users_dict: dict[JID, dict[str, str | None]] ) -> Iq: iq = Iq(typ="set", to=room_jid, queryNS=Namespace.MUC_ADMIN) item = iq.setQuery() for jid, data in users_dict.items(): affiliation = data.get("affiliation") reason = data.get("reason") nick = data.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: JID, reason: str | None, jid: str | None) -> Iq: 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: JID, form) -> Iq: 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: JID) -> Iq: iq = Iq(typ="get", queryNS=Namespace.MUC_OWNER, to=room_jid) return iq def make_cancel_config_request(room_jid: JID) -> Iq: 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: JID, nick: str, role: RoleT, reason: str | None ) -> Iq: 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: JID, form_node) -> Iq: 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: JID, to: JID, reason: str | None, password: str | None, continue_: bool | None ) -> Message: 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: JID, to: JID, reason: str | None, password: str | None, continue_: bool | None ) -> Message: 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: Node, is_presence: bool = True) -> MucUserData | None: 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, force_bare=True) 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/muclumbus.py000066400000000000000000000104531524361513300270210ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import AnonymityMode from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.dataforms import extend_form from nbxmpp.modules.util import finalize from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import Node from nbxmpp.structs import MuclumbusItem from nbxmpp.structs import MuclumbusResult from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client # API Documentation # https://search.jabber.network/docs/api class Muclumbus(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [] @iq_request_task def request_parameters(self, jid: str): 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: str, dataform, items_per_page: int = 50, after: str | None = 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: list[MuclumbusItem] = [] 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 ) def _make_parameter_request(jid: str) -> Iq: query = Iq(to=jid, typ="get") query.addChild(node=Node("search", attrs={"xmlns": Namespace.MUCLUMBUS})) return query def _make_search_query( jid: str, dataform, items_per_page: int = 50, after: str | None = None ) -> Iq: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/nickname.py000066400000000000000000000103521524361513300265700ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import PresenceType from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import Presence from nbxmpp.structs import MessageProperties from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Nickname(BaseModule): _depends = {"publish": "PubSub"} def __init__(self, client: Client) -> None: 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: Client, stanza: Message | Presence, properties: MessageProperties | PresenceProperties, ) -> None: 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"). # Usage is not recommended in MUC, but it is a workaround # to allow code points forbidden in resource parts in nicknames. if not properties.from_muc and properties.type in ( PresenceType.AVAILABLE, PresenceType.UNAVAILABLE, ): return properties.nickname = self._parse_nickname(stanza) def _process_pubsub_nickname( self, _client: Client, _stanza: Message, properties: MessageProperties ) -> None: 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: Node) -> str | None: 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: str | None, public: bool = 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: bool): 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/ogp.py000066400000000000000000000040051524361513300255660ustar00rootroot00000000000000# Copyright (C) 2022 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from dataclasses import fields from nbxmpp import Node from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.structs import MessageProperties from nbxmpp.structs import OpenGraphData from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client from nbxmpp.protocol import Message class OpenGraph(BaseModule): def __init__(self, client: Client): BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="message", callback=self._process_message_opengraph, ns=Namespace.RDF, priority=15, ), ] def _process_message_opengraph( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: for desc in stanza.getTags("Description", namespace=Namespace.RDF): about = desc.getAttr("about") or desc.getNamespacedAttr( "about", Namespace.RDF ) if about: properties.open_graph[about] = self._process_description(desc) @staticmethod def _process_description(description: Node) -> OpenGraphData: data = OpenGraphData() for field in fields(OpenGraphData): tag = field.name if tag == "image": for node in description.getTags(tag, namespace=Namespace.OPEN_GRAPH): node_data = node.getData() if node_data.startswith("data:image"): setattr(data, tag, node_data) break else: if node := description.getTag(tag, namespace=Namespace.OPEN_GRAPH): setattr(data, tag, node.getData()) return data python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/omemo.py000066400000000000000000000372021524361513300261220ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Literal from typing import TYPE_CHECKING from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import MessageProperties from nbxmpp.structs import OMEMOBundle from nbxmpp.structs import OMEMOMessage from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task from nbxmpp.util import b64decode from nbxmpp.util import b64encode from nbxmpp.util import from_xs_boolean if TYPE_CHECKING: from nbxmpp.client import Client class OMEMO(BaseModule): _depends = { "publish": "PubSub", "request_items": "PubSub", } def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: set[int] | None = None, public: bool = True): task = yield 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: str | None = 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: OMEMOBundle, device_id: int, public: bool = True): task = yield 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: str, device_id: int): task = yield 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], device_id) def _parse_omemo_message(stanza: Message) -> OMEMOMessage: """
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: raise MalformedStanzaError("sid attr not found", stanza) iv_node = header.getTag("iv") try: iv = b64decode(iv_node.getData()) 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()) 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: dict[int, tuple[bytes, bool]] = {} 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()), 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: Node | None, device_id: int) -> OMEMOBundle: """ 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", namespace=Namespace.OMEMO_TEMP ) try: result["spk"] = {"key": b64decode(signed_prekey_node.getData())} 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", namespace=Namespace.OMEMO_TEMP ) try: result["spk_signature"] = b64decode(signed_signature_node.getData()) except Exception as error: error = "Failed to decode signedPreKeySignature: %s" % error raise MalformedStanzaError(error, item) identity_key_node = bundle.getTag("identityKey", namespace=Namespace.OMEMO_TEMP) try: result["ik"] = b64decode(identity_key_node.getData()) except Exception as error: error = "Failed to decode IdentityKey: %s" % error raise MalformedStanzaError(error, item) prekeys_node = bundle.getTag("prekeys", namespace=Namespace.OMEMO_TEMP) if prekeys_node is None: raise MalformedStanzaError("No prekeys node found", item) prekeys = prekeys_node.getTags("preKeyPublic", namespace=Namespace.OMEMO_TEMP) if not prekeys: raise MalformedStanzaError("No prekeys found", item) result["otpks"] = [] for prekey in prekeys: try: id_ = int(prekey.getAttr("preKeyId")) except Exception as error: raise MalformedStanzaError("Invalid prekey: %s" % error, item) try: key = b64decode(prekey.getData()) except Exception as error: raise MalformedStanzaError( "Failed to decode preKeyPublic: %s" % error, item ) result["otpks"].append({"key": key, "id": id_}) return OMEMOBundle(**result, device_id=device_id, namespace=Namespace.OMEMO_TEMP) def _make_bundle(bundle: OMEMOBundle) -> Node: """ 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: set[int] | None) -> Node: if devicelist is None: devicelist = set() 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: Node) -> list[int]: """ """ list_node = item.getTag("list", namespace=Namespace.OMEMO_TEMP) if list_node is None: raise MalformedStanzaError("No list node found", item) result: list[int] = [] for dn in list_node.getTags("device", namespace=Namespace.OMEMO_TEMP): _id = dn.getAttr("id") if _id is None: raise MalformedStanzaError("Device node without id attribute", item) try: result.append(int(_id)) except Exception: raise MalformedStanzaError("Non integer device id", item) return result def create_omemo_message( stanza: Message, omemo_message: OMEMOMessage, store_hint: bool = True, node_whitelist: list[tuple[str, str]] | None = None, ) -> 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: Literal["chat", "groupchat"], jid: str, omemo_message: OMEMOMessage ) -> 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: Message, node_whitelist: list[tuple[str, str]]) -> None: whitelisted_nodes: list[Node] = [] 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/oob.py000066400000000000000000000025231524361513300255630ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.structs import MessageProperties from nbxmpp.structs import OOBData from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class OOB(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/openpgp.py000066400000000000000000000317361524361513300264640ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import TYPE_CHECKING import secrets import string import time from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.modules.util import finalize from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import StanzaMalformed from nbxmpp.structs import MessageProperties from nbxmpp.structs import PGPKeyMetadata from nbxmpp.structs import PGPPublicKey from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task from nbxmpp.util import b64decode from nbxmpp.util import b64encode if TYPE_CHECKING: from nbxmpp.client import Client class OpenPGP(BaseModule): _depends = { "publish": "PubSub", "request_items": "PubSub", } def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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) except Exception: self._log.warning("b64decode failed") self._log.warning(stanza) return def _process_pubsub_openpgp( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: """ """ 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: list[PGPKeyMetadata] | None, public: bool = True ) -> Any: task = yield access_model = "open" if public else "presence" options = { "pubsub#persist_items": "true", "pubsub#access_model": access_model, } 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: bytes, fingerprint: str, date: float, public: bool = True ) -> Any: task = yield access_model = "open" if public else "presence" options = { "pubsub#persist_items": "true", "pubsub#access_model": access_model, } date_str = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(date)) result = yield self.publish( f"{Namespace.OPENPGP_PK}:{fingerprint.upper()}", _make_public_key(key, date_str), id_=date_str, options=options, force_node_options=True, ) yield finalize(task, result) @iq_request_task def request_public_key(self, jid: JID, fingerprint: str) -> Any: task = yield items = yield self.request_items( f"{Namespace.OPENPGP_PK}:{fingerprint.upper()}", 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: JID | None = None) -> Any: task = yield 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) -> Any: task = yield 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: bytes) -> Any: task = yield 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: Node) -> tuple[list[Node | str], list[JID], str]: """ """ 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: list[JID] = [] 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 not payload.getChildren(): raise StanzaMalformed("Invalid payload node") return payload.getChildren(), recipients, timestamp def create_signcrypt_node( stanza: Node, recipients: list[JID], not_encrypted_nodes: list[tuple[str, str]] ) -> Node: """ """ encrypted_nodes: list[Node] = [] child_nodes = list(stanza.getChildren()) for node in child_nodes: if isinstance(node, str): stanza.delChild(node) continue 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() -> str: rpad_range = secrets.randbelow(21) + 30 return "".join(secrets.choice(string.ascii_letters) for _ in range(rpad_range)) def create_message_stanza( stanza: Message, encrypted_payload: str | bytes, with_eme: bool ) -> None: b64encoded_payload = b64encode(encrypted_payload) openpgp_node = Node("openpgp", attrs={"xmlns": Namespace.OPENPGP}) openpgp_node.addData(b64encoded_payload) stanza.addChild(node=openpgp_node) if with_eme: eme_node = Node( "encryption", attrs={"xmlns": Namespace.EME, "namespace": Namespace.OPENPGP} ) stanza.addChild(node=eme_node) def _make_keylist(keylist: list[PGPKeyMetadata] | None) -> Node: 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.upper(), "date": date} item.addChild("pubkey-metadata", attrs=attrs) return item def _make_public_key(key: str | bytes, date: str) -> Node: # date attribute is added for backwards compatibility 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: str | bytes | None) -> Node: item = Node("secretkey", {"xmlns": Namespace.OPENPGP}) if secret_key is not None: item.setData(b64encode(secret_key)) return item def _parse_public_key(jid: JID, item: Node) -> PGPPublicKey: pub_key = item.getTag("pubkey", namespace=Namespace.OPENPGP) if pub_key is None: raise ValueError("pubkey node missing") data = pub_key.getTag("data") if data is None: raise ValueError("data node missing") try: key = b64decode(data.getData()) except Exception as error: raise ValueError(f"decoding error: {error}") # Set date to 0 because the attribute is deprecated return PGPPublicKey(jid, key, 0) def _parse_keylist(jid: JID, item: Node) -> list[PGPKeyMetadata] | None: 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: list[PGPKeyMetadata] = [] 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 not isinstance(timestamp, float): raise ValueError("Invalid date timestamp: %s" % date) data.append(PGPKeyMetadata(jid, fingerprint, timestamp)) return data def _parse_secret_key(item: Node) -> bytes: 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) except Exception as error: raise ValueError(f"decoding error: {error}") return key python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/pgplegacy.py000066400000000000000000000037431524361513300267640ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Presence from nbxmpp.structs import MessageProperties from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class PGPLegacy(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Presence, properties: PresenceProperties ) -> None: signed = stanza.getTag("x", namespace=Namespace.SIGNED) if signed is None: return properties.signed = signed.getData() def _process_pgplegacy_message( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/ping.py000066400000000000000000000030751524361513300257440ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import IqProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Ping(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Iq, _properties: IqProperties ) -> None: 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: JID): _task = yield response = yield _make_ping_request(jid) yield process_response(response) def _make_ping_request(jid: JID) -> Iq: iq = Iq("get", to=jid) iq.addChild(name="ping", namespace=Namespace.PING) return iq python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/presence.py000066400000000000000000000127141524361513300266130ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Literal from typing import TYPE_CHECKING from nbxmpp.const import PresenceShow from nbxmpp.const import PresenceType from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import log_calls from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_BAD_REQUEST from nbxmpp.protocol import Error as ErrorStanza from nbxmpp.protocol import JID from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Presence from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler from nbxmpp.util import error_factory if TYPE_CHECKING: from nbxmpp.client import Client class BasePresence(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Presence, properties: PresenceProperties ) -> None: 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: Presence) -> int: 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: Presence) -> PresenceType: 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: Presence) -> PresenceShow: 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 @log_calls def unsubscribe(self, jid: JID) -> None: self.send(jid=jid, typ="unsubscribe") @log_calls def unsubscribed(self, jid: JID) -> None: self.send(jid=jid, typ="unsubscribed") @log_calls def subscribed(self, jid: JID) -> None: self.send(jid=jid, typ="subscribed") @log_calls def subscribe( self, jid: JID, status: str | None = None, nick: str | None = None, preauth: str | None = None, ) -> None: self.send(jid=jid, typ="subscribe", status=status, nick=nick, preauth=preauth) def send( self, jid: JID | None = None, typ: str | None = None, priority: int | None = None, show: Literal["chat", "away", "xa", "dnd"] | None = None, status: str | None = None, nick: str | None = None, caps: dict[str, str] | None = None, idle_time=None, signed=None, muc: bool = False, muc_history=None, muc_password: str | None = None, preauth: str | None = None, extend=None, ) -> None: if show is not None and show not in ("chat", "away", "xa", "dnd"): raise ValueError("Invalid show value: %s" % show) presence = Presence(jid, typ, priority, show, status) if nick is not None: nick_tag = presence.setTag("nick", namespace=Namespace.NICK) nick_tag.setData(nick) if idle_time is not None: idle_node = presence.setTag("idle", namespace=Namespace.IDLE) idle_node.setAttr("since", idle_time) if caps is not None and typ != "unavailable": presence.setTag("c", namespace=Namespace.CAPS, attrs=caps) if signed is not None: presence.setTag(Namespace.SIGNED + " x").setData(signed) if muc or muc_history is not None or muc_password is not None: muc_x = presence.setTag(Namespace.MUC + " x") if muc_history is not None: muc_x.setTag("history", muc_history) if muc_password is not None: muc_x.setTagData("password", muc_password) if preauth is not None: presence.setTag(Namespace.PARS + " preauth", attrs={"token": preauth}) if extend is not None: for node in extend: presence.addChild(node=node) self._client.send_stanza(presence) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/pubsub.py000066400000000000000000000453671524361513300263210ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import Literal from typing import NamedTuple from typing import TYPE_CHECKING from dataclasses import dataclass from nbxmpp.const import MessageType from nbxmpp.errors import is_error from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import PubSubStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.dataforms import extend_form from nbxmpp.modules.util import finalize from nbxmpp.modules.util import process_response from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.structs import CommonResult from nbxmpp.structs import MessageProperties from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class PubSub(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: if properties.type not in (MessageType.HEADLINE, MessageType.NORMAL): return properties.pubsub = True assert properties.jid is not None if properties.jid.is_full: self._log.warning("PubSub event from full jid %s", properties.jid) self._log.warning(stanza) return 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: str, id_: str, jid: JID | None = 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: str, max_items: int | None = None, jid: JID | None = 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: str, item: Node, id_: str | None = None, options: dict[str, str] | None = None, jid: JID | None = None, force_node_options: bool = 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: str): _task = yield 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: str, model: Literal["open", "presence"]): 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 result = yield self.set_node_configuration(node, result.form) yield finalize(task, result) @iq_request_task def retract(self, node: str, id_: str, jid: JID | None = None, notify: bool = True): _task = yield response = yield _make_retract_request(node, id_, jid, notify) yield process_response(response) @iq_request_task def purge(self, node: str, jid: JID | None = None): _task = yield response = yield _make_purge_request(node, jid) yield process_response(response) @iq_request_task def delete(self, node: str, jid: JID | None = None): _task = yield response = yield _make_delete_request(node, jid) yield process_response(response) @iq_request_task def reconfigure_node(self, node: str, options: dict[str, str], 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: Node, form, jid: JID | None = None): _task = yield response = yield _make_node_configuration(node, form, jid) yield process_response(response) @iq_request_task def get_node_configuration(self, node: str, jid: JID | None = 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) @iq_request_task def get_subscriptions(self, jid: JID): _task = yield response = yield _make_get_subscription_request(jid) if response.isError(): raise PubSubStanzaError(response) pubsub_node = response.getTag("pubsub", namespace=Namespace.PUBSUB) if pubsub_node is None: raise MalformedStanzaError("pubsub node missing", response) subscriptions = pubsub_node.getTag("subscriptions", namespace=Namespace.PUBSUB) if subscriptions is None: raise MalformedStanzaError("subscriptions node missing", response) result = [] for item in subscriptions.getTags("subscription", namespace=Namespace.PUBSUB): try: sub = PubSubSubscription.from_response(item) except MalformedStanzaError as error: self._log.warning(error) self._log.warning(error.stanza) continue result.append(sub) yield result @iq_request_task def subscribe(self, node: str, jid: str | None = None): _task = yield own_jid = self._client.get_bound_jid().new_as_bare() response = yield _make_subscribe_request(node, own_jid, jid) if response.isError(): raise PubSubStanzaError(response) item = _get_subscription_item(response) if item is None: raise MalformedStanzaError("subscription node missing", response) yield PubSubSubscription.from_response(item) @iq_request_task def unsubscribe(self, node: str, jid: str | None = None): _task = yield own_jid = self._client.get_bound_jid().new_as_bare() response = yield _make_unsubscribe_request(node, own_jid, jid) yield process_response(response) def get_pubsub_request( jid: JID, node: str, id_: str | None = None, max_items: int | None = None ) -> Iq: 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: Node) -> Node | None: pubsub_node = stanza.getTag("pubsub") items_node = pubsub_node.getTag("items") return items_node.getTag("item") def get_pubsub_items(stanza: Node, node: str | None = None) -> list[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: dict[str, str]) -> Node: 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: Iq, node: str) -> list[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: Iq, node: str, id_: str) -> Node | None: 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 _get_subscription_item(response: Iq) -> Node | None: pubsub_node = response.getTag("pubsub", namespace=Namespace.PUBSUB) if pubsub_node is None: raise MalformedStanzaError("pubsub node missing", response) return pubsub_node.getTag("subscription", namespace=Namespace.PUBSUB) def _make_pubsub_request( node: str, id_: str | None = None, max_items: int | None = None, jid: JID | None = None, ) -> Iq: 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: Iq, node: str): 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: Iq, node: str, id_: str | None) -> str | None: 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: str, item: Node, id_: str | None, options: dict[str, str] | None, jid: JID | None, ) -> Iq: 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: dict[str, str]) -> Node: 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: str, id_: str, jid: JID | None, notify: bool) -> Iq: 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_purge_request(node: str, jid: JID | None) -> Iq: query = Iq("set", to=jid) pubsub = query.addChild("pubsub", namespace=Namespace.PUBSUB_OWNER) pubsub.addChild("purge", attrs={"node": node}) return query def _make_delete_request(node: str, jid: JID | None) -> Iq: 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: Node, form, jid: JID | None) -> Iq: 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: str, jid: JID | None) -> Iq: query = Iq("get", to=jid) pubsub = query.addChild("pubsub", namespace=Namespace.PUBSUB_OWNER) pubsub.addChild("configure", {"node": node}) return query def _make_get_subscription_request( jid: JID | None = None, ) -> Iq: query = Iq("get", to=jid) pubsub = query.addChild("pubsub", namespace=Namespace.PUBSUB) pubsub.addChild("subscriptions") return query def _make_subscribe_request(node: str, own_jid: JID, jid: JID | None = None) -> Iq: query = Iq("set", to=jid) pb = query.addChild("pubsub", namespace=Namespace.PUBSUB) pb.addChild("subscribe", {"node": node, "jid": str(own_jid)}) return query def _make_unsubscribe_request(node: str, own_jid: JID, jid: JID | None = None) -> Iq: query = Iq("set", to=jid) pb = query.addChild("pubsub", namespace=Namespace.PUBSUB) pb.addChild("unsubscribe", {"node": node, "jid": str(own_jid)}) return query def _apply_options(form, options: dict[str, str]) -> None: for var, value in options.items(): try: field = form[var] except KeyError: pass else: field.value = value class PubSubNodeConfigurationResult(NamedTuple): jid: JID node: str form: Any class PubSubPublishResult(NamedTuple): jid: JID node: str id: str @dataclass class PubSubSubscription: node: str jid: JID subscription: str subid: str | None @classmethod def from_response(cls, sub_item: Iq) -> PubSubSubscription: node = sub_item.getAttr("node") if not node: raise MalformedStanzaError("node attr is missing", sub_item) jid = sub_item.getAttr("jid") if not jid: raise MalformedStanzaError("jid attr is missing", sub_item) try: jid = JID.from_string(jid) except Exception: raise MalformedStanzaError("jid is invalidvalid", sub_item) subscription = sub_item.getAttr("subscription") if not subscription: raise MalformedStanzaError("subscription attr is missing", sub_item) if subscription not in ("none", "pending", "subscribed", "unconfigured"): raise MalformedStanzaError( "unknown subscription value '%s'" % subscription, sub_item ) subid = sub_item.getAttr("subid") return cls(node=node, jid=jid, subscription=subscription, subid=subid) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/reactions.py000066400000000000000000000033701524361513300267740ustar00rootroot00000000000000# Copyright (C) 2022 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.structs import MessageProperties from nbxmpp.structs import Reactions as ReactionStruct from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client from nbxmpp.protocol import Message class Reactions(BaseModule): def __init__(self, client: Client): BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="message", callback=self._process_message_reaction, ns=Namespace.REACTIONS, priority=15, ), ] def _process_message_reaction( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: reactions = stanza.getTag("reactions", namespace=Namespace.REACTIONS) if reactions is None: return id_ = reactions.getAttr("id") if not id_: self._log.warning("Reactions without ID") return emojis: set[str] = set() for reaction in reactions.getTags("reaction"): # we strip for clients that might add white spaces and/or # new lines in the reaction content. emoji = reaction.getData().strip() if emoji: emojis.add(emoji) else: self._log.warning("Empty reaction") self._log.warning(stanza) properties.reactions = ReactionStruct(id_, emojis) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/receipts.py000066400000000000000000000052051524361513300266220ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import isMucPM from nbxmpp.protocol import Message from nbxmpp.structs import MessageProperties from nbxmpp.structs import ReceiptData from nbxmpp.structs import StanzaHandler from nbxmpp.util import generate_id if TYPE_CHECKING: from nbxmpp.client import Client class Receipts(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: Message | Any) -> Message: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/register/000077500000000000000000000000001524361513300262545ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/register/__init__.py000066400000000000000000000000551524361513300303650ustar00rootroot00000000000000from .register import Register # noqa: F401 python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/register/register.py000066400000000000000000000065031524361513300304560ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import ChangePasswordStanzaError from nbxmpp.errors import RegisterStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule 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 _make_register_form from nbxmpp.modules.register.util import _make_unregister_request from nbxmpp.modules.register.util import _parse_register_data from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.task import iq_request_task from nbxmpp.util import get_form if TYPE_CHECKING: from nbxmpp.client import Client class Register(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [] @iq_request_task def unregister(self, jid: JID | None = None): _task = yield response = yield _make_unregister_request(jid) yield process_response(response) @iq_request_task def send_preauth(self, token: str, jid: JID | None = None): _task = yield if jid is None: jid = self._client.domain iq = Iq("set", to=jid) iq.addChild("preauth", namespace=Namespace.PARS, attrs={"token": token}) response = yield iq yield process_response(response) @iq_request_task def request_register_form(self, jid: JID | None = 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: JID | None = 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: str): _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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/register/util.py000066400000000000000000000072121524361513300276050ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from nbxmpp.const import REGISTER_FIELDS from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.bits_of_binary import parse_bob_data from nbxmpp.modules.dataforms import create_field from nbxmpp.modules.dataforms import extend_form from nbxmpp.modules.dataforms import FieldT from nbxmpp.modules.dataforms import SimpleDataForm from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import Protocol from nbxmpp.structs import RegisterData def _make_password_change_request(domain: str, username: str, password: str) -> Iq: 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: str, form) -> Iq: iq = Iq("set", Namespace.REGISTER, to=domain) iq.setQueryPayload(form) return iq def _make_register_form(jid: JID, form) -> Iq: 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: JID) -> Iq: iq = Iq("set", to=jid) query = iq.setQuery() query.setNamespace(Namespace.REGISTER) query.addChild("remove") return iq def _parse_oob_url(query: Iq) -> str | None: 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: Protocol) -> SimpleDataForm | None: fields: list[FieldT] = [] 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: Protocol) -> RegisterData: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/replies.py000066400000000000000000000031711524361513300264470ustar00rootroot00000000000000# This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later # XEP-0461: Message Replies from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.structs import MessageProperties from nbxmpp.structs import ReplyData from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Replies(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="message", callback=self._process_message, ns=Namespace.REPLY, priority=15, ) ] def _process_message( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: reply = stanza.getTag("reply", namespace=Namespace.REPLY) if reply is None: return to = reply.getAttr("to") if to is None: self._log.warning('Received reply without "to" attribute') return try: reply_to = JID.from_string(to) except Exception: self._log.warning("Invalid jid on reply element: %s", to) return reply_to_id = reply.getAttr("id") if reply_to_id is None: self._log.warning('Received reply without "id"') return properties.reply_data = ReplyData(to=reply_to, id=reply_to_id) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/retraction.py000066400000000000000000000051411524361513300271550ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import datetime as dt from nbxmpp import Namespace from nbxmpp import Node from nbxmpp.modules.base import BaseModule from nbxmpp.modules.date_and_time import parse_datetime from nbxmpp.structs import MessageProperties from nbxmpp.structs import RetractionData from nbxmpp.structs import StanzaHandler if TYPE_CHECKING: from nbxmpp.client import Client class Retraction(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="message", callback=self._process_message_retraction, ns=Namespace.MESSAGE_RETRACT_1, priority=20, ), ] def _process_message_retraction( self, _client: Client, stanza: Node, properties: MessageProperties ) -> None: retract, is_tombstone = _get_retract_element(stanza, properties) if retract is None: return retract_id = retract.getAttr("id") if not retract_id: self._log.warning(" without id") self._log.warning(stanza) return timestamp = _parse_retraction_timestamp(retract, is_tombstone, properties) properties.retraction = RetractionData( id=retract_id, is_tombstone=is_tombstone, timestamp=timestamp ) def _get_retract_element( element: Node, properties: MessageProperties ) -> tuple[Node | None, bool]: """ returns a tuple Node and a boolean value which signals if it is a tombstone """ retract = element.getTag("retract", namespace=Namespace.MESSAGE_RETRACT_1) if retract is not None: return retract, False if not properties.is_mam_message: return None, False retracted = element.getTag("retracted", namespace=Namespace.MESSAGE_RETRACT_1) if retracted is not None: return retracted, True return None, False def _parse_retraction_timestamp( retract: Node, is_tombstone: bool, properties: MessageProperties ) -> dt.datetime: if is_tombstone: stamp_attr = retract.getAttr("stamp") stamp = parse_datetime(stamp_attr, check_utc=True, convert="utc") if stamp is not None: assert isinstance(stamp, dt.datetime) return stamp if properties.is_mam_message: assert properties.mam is not None stamp = properties.mam.timestamp else: stamp = properties.timestamp return dt.datetime.fromtimestamp(stamp, dt.timezone.utc) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/roster.py000066400000000000000000000126131524361513300263230ustar00rootroot00000000000000# Copyright (C) 2021 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import NodeProcessed from nbxmpp.simplexml import Node from nbxmpp.structs import IqProperties from nbxmpp.structs import RosterData from nbxmpp.structs import RosterItem from nbxmpp.structs import RosterPush from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Roster(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="iq", callback=self._process_roster_push, typ="set", priority=15, ns=Namespace.ROSTER, ), ] @iq_request_task def request_roster(self, version: str | None = None): _task = yield ver_support = self._client.features.has_roster_version() if not ver_support: version = None if ver_support and version is None: version = "" self._log.info("Roster versioning supported: %s", ver_support) response = yield _make_request(version, ver_support) if response.isError(): raise StanzaError(response) query = response.getTag("query", namespace=Namespace.ROSTER) if query is None: if not ver_support: raise MalformedStanzaError("query node missing", response) yield RosterData(None, version) pushed_items, version = self._parse_push(response, ver_support) yield RosterData(pushed_items, version) def _process_roster_push( self, _client: Client, stanza: Iq, properties: IqProperties ) -> None: from_ = stanza.getFrom() if from_ is not None: if not self._client.get_bound_jid().bare == from_: self._log.warning("Malicious Roster Push from %s", from_) raise NodeProcessed ver_support = self._client.features.has_roster_version() pushed_items, version = self._parse_push(stanza, ver_support) if len(pushed_items) != 1: self._log.warning( "Roster push does not have exactly one item: %s", pushed_items ) self._log.warning(stanza) raise NodeProcessed item = pushed_items[0] properties.roster = RosterPush(item, version) self._log.info("Roster Push, version: %s", properties.roster.version) self._log.info(item) self._ack_roster_push(stanza) def _ack_roster_push(self, stanza: Iq) -> None: iq = Iq( "result", to=stanza.getFrom(), frm=stanza.getTo(), attrs={"id": stanza.getID()}, ) self._client.send_stanza(iq) @iq_request_task def delete_item(self, jid: JID | str): _task = yield response = yield _make_delete(jid) yield process_response(response) @iq_request_task def set_item(self, jid: JID, name: str | None, groups: set[str] | None = None): _task = yield response = yield _make_set(jid, name, groups) yield process_response(response) def _parse_push( self, stanza: Iq, ver_support: bool ) -> tuple[list[RosterItem], str | None]: query = stanza.getTag("query", namespace=Namespace.ROSTER) version = None if ver_support: version = query.getAttr("ver") if version is None: # raise MalformedStanzaError('ver attribute missing', stanza) # Prosody sometimes does not send ver attribute with some # community modules self._log.warning("no version attribute found") pushed_items: list[RosterItem] = [] for item in query.getTags("item"): try: roster_item = RosterItem.from_node(item) except Exception as e: self._log.warning("Invalid roster item: %s", e) self._log.warning(stanza) continue pushed_items.append(roster_item) return pushed_items, version def _make_delete(jid: JID | str) -> Iq: return Iq( "set", Namespace.ROSTER, payload=[Node("item", {"jid": jid, "subscription": "remove"})], ) def _make_set(jid: JID, name: str | None, groups: set[str] | None = None) -> Iq: if groups is None: groups = set() infos: dict[str, JID | str] = {"jid": jid} if name: infos["name"] = name iq = Iq("set", Namespace.ROSTER) query = iq.setQuery() item = query.addChild("item", attrs=infos) for group in groups: item.addChild("group").setData(group) return iq def _make_request(version: str | None, roster_ver_support: bool) -> Iq: iq = Iq("get", Namespace.ROSTER) if version is None: version = "" if roster_ver_support: iq.setTagAttr("query", "ver", version) return iq python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/rsm.py000066400000000000000000000023451524361513300256070ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Node from nbxmpp.structs import RSMData def parse_rsm(stanza: Node) -> RSMData | None: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/security_labels.py000066400000000000000000000115041524361513300301740ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING import hashlib from dataclasses import dataclass from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import Message from nbxmpp.simplexml import Node from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task from nbxmpp.util import from_xs_boolean if TYPE_CHECKING: from nbxmpp.client import Client class SecurityLabels(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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: str): _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) try: restrict = from_xs_boolean(catalog_node.getAttr("restrict")) except Exception: restrict = False items = catalog_node.getTags("item") labels: dict[str, SecurityLabel | None] = {} default: str | None = None for item in items: selector = item.getAttr("selector") if selector is None: continue if item.getAttr("default") == "true": default = selector security = item.getTag("securitylabel", namespace=Namespace.SECLABEL) if security is None: labels[selector] = None continue try: security_label = SecurityLabel.from_node(security) except ValueError: continue labels[selector] = security_label yield Catalog(labels=labels, default=default, restrict=restrict) def _make_catalog_request(domain: str | None, jid: str) -> Iq: 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) -> Node: 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: Node) -> Displaymarking: return cls( name=node.getData(), fgcolor=node.getAttr("fgcolor") or "#000", bgcolor=node.getAttr("bgcolor") or "#FFF", ) @dataclass class SecurityLabel: displaymarking: Displaymarking | None label: Node def to_node(self) -> Node: 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: Node) -> SecurityLabel: 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) def get_label_hash(self) -> str: sha = hashlib.sha512() sha.update(str(self.label).encode()) return sha.hexdigest() @dataclass class Catalog: labels: dict[str, SecurityLabel | None] default: str restrict: bool def get_label_names(self) -> list[str]: return list(self.labels.keys()) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/software_version.py000066400000000000000000000071411524361513300304040ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from collections.abc import Callable from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import ERR_FORBIDDEN from nbxmpp.protocol import ERR_SERVICE_UNAVAILABLE from nbxmpp.protocol import Error from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import IqProperties from nbxmpp.structs import SoftwareVersionResult from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class SoftwareVersion(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="iq", callback=self._answer_request, typ="get", priority=60, ns=Namespace.VERSION, ), ] self._name: str | None = None self._version: str | None = None self._os: str | None = None self._enabled = False self._allow_reply_func: Callable[..., bool] | None = None def disable(self) -> None: self._enabled = False def set_allow_reply_func(self, func: Callable[..., bool]) -> None: self._allow_reply_func = func @iq_request_task def request_software_version(self, jid: JID): _task = yield 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: str, version: str, os: str | None = None ) -> None: self._name, self._version, self._os = name, version, os self._enabled = True def _answer_request( self, _client: Client, stanza: Iq, _properties: IqProperties ) -> None: self._log.info("Request received from %s", stanza.getFrom()) if not self._enabled or self._name is None or self._version is None: self._client.send_stanza(Error(stanza, ERR_SERVICE_UNAVAILABLE)) raise NodeProcessed if self._allow_reply_func is not None: if not self._allow_reply_func(stanza.getFrom()): self._client.send_stanza(Error(stanza, ERR_FORBIDDEN)) raise NodeProcessed 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: Iq) -> SoftwareVersionResult: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/tune.py000066400000000000000000000051101524361513300257520ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import TUNE_DATA from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.structs import TuneData from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client class Tune(BaseModule): _depends = {"publish": "PubSub"} def __init__(self, client: Client) -> None: 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: Client, _stanza: Message, properties: MessageProperties ) -> None: 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: TuneData | None): 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/user_avatar.py000066400000000000000000000246761524361513300273350ustar00rootroot00000000000000# Copyright (C) 2019 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Literal from typing import TYPE_CHECKING import hashlib from collections.abc import Iterator from dataclasses import asdict from dataclasses import dataclass from dataclasses import field from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task from nbxmpp.util import b64decode from nbxmpp.util import b64encode if TYPE_CHECKING: from nbxmpp.client import Client class UserAvatar(BaseModule): _depends = { "publish": "PubSub", "request_item": "PubSub", "request_items": "PubSub", "purge": "PubSub", } def __init__(self, client: Client) -> None: 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: Client, stanza: Message, properties: MessageProperties ) -> None: 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_: str, jid: JID | None = 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: JID | None = 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: Avatar | None, public: bool = 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.purge(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: Avatar, access_model: Literal["open", "presence"] ): 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) 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: AvatarMetaData | None, access_model: Literal["open", "presence"] ): task = yield 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: bool): 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: Node, id_: str) -> AvatarData: 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) except Exception as error: raise MalformedStanzaError(f"decoding error: {error}", item) avatar_sha = hashlib.sha1(avatar).hexdigest() if avatar_sha != id_: raise MalformedStanzaError("avatar does not match sha", item) return AvatarData(data=avatar, sha=avatar_sha) def _make_metadata_node(infos: list[AvatarInfo]) -> Node: 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: AvatarData) -> Node: item = Node("data", attrs={"xmlns": Namespace.AVATAR_DATA}) item.setData(b64encode(avatar.data)) return item def _get_info_attrs( avatar: bytes, avatar_sha: str, height: int | None, width: int | None ) -> dict[str, str | int]: 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 = None height: int | None = None width: int | None = None def __post_init__(self) -> None: 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) -> dict[str, str | int]: 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) -> int: return hash(self.id) @dataclass class AvatarData: data: bytes sha: str @dataclass class AvatarMetaData: infos: list[AvatarInfo] = field(default_factory=list) default: str | None = None @classmethod def from_node(cls, node: Node, default: str | None = None) -> AvatarMetaData: infos: list[AvatarInfo] = [] 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: AvatarInfo, make_default: bool = False ) -> None: 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) -> list[str]: return [info.id for info in self.infos] @dataclass class Avatar: metadata: AvatarMetaData = field(default_factory=AvatarMetaData) data: dict[AvatarInfo, AvatarData] = field(init=False, default_factory=dict) def add_image_source( self, data: bytes, type_: str, height: int, width: int, url: str | None = None, make_default: bool = True, ) -> None: 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) -> Iterator[tuple[AvatarInfo, AvatarData]]: for info, data in self.data.items(): if info.url is not None: continue yield info, data python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/util.py000066400000000000000000000050201524361513300257540ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import ParamSpec from typing import TYPE_CHECKING from typing import TypeVar import functools import inspect import logging from collections.abc import Callable from urllib.parse import unquote from urllib.parse import urlparse from nbxmpp.errors import is_error from nbxmpp.errors import StanzaError from nbxmpp.protocol import Iq from nbxmpp.simplexml import Node from nbxmpp.structs import CommonResult if TYPE_CHECKING: from nbxmpp.dispatcher import NBXMPPModuleT from nbxmpp.task import Task T = TypeVar("T") P = ParamSpec("P") def process_response(response: Iq) -> CommonResult: if response.isError(): raise StanzaError(response) return CommonResult(jid=response.getFrom()) def raise_if_error(result: Any) -> None: if is_error(result): raise result def finalize(task: Task, result: Any) -> Any: if is_error(result): raise result if isinstance(result, Node): return task.set_result(result) return result def parse_xmpp_uri(uri: str) -> tuple[str, str, dict[str, str]]: 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_: dict[str, str] = {} for key_value in key_value_pairs: key, value = key_value.split("=") dict_[key] = unquote(value) return (url.path, action, dict_) def make_func_arguments_string( func: Callable[..., Any], self: Any, args: Any, kwargs: Any ) -> str: signature = inspect.signature(func) bound_arguments = signature.bind(self, *args, **kwargs) bound_arguments.apply_defaults() arg_string = "" for name, arg in bound_arguments.arguments.items(): if name == "self": continue arg_string += f"{name}={arg}, " arg_string = arg_string[:-2] return f"{func.__name__}({arg_string})" def log_calls(func: Callable[P, T]) -> Callable[P, T]: @functools.wraps(func) def func_wrapper(self: NBXMPPModuleT, *args: Any, **kwargs: Any) -> T: if self._log.isEnabledFor(logging.INFO): # type: ignore self._log.info(make_func_arguments_string(func, self, args, kwargs)) # type: ignore return func(self, *args, **kwargs) return func_wrapper python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/vcard4.py000066400000000000000000001071031524361513300261670ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import Literal from typing import TYPE_CHECKING import logging from collections import defaultdict from collections.abc import ValuesView from dataclasses import dataclass from dataclasses import field from nbxmpp.errors import is_error from nbxmpp.errors import MalformedStanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import finalize from nbxmpp.modules.util import raise_if_error from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.simplexml import Node from nbxmpp.structs import MessageProperties from nbxmpp.structs import StanzaHandler from nbxmpp.task import iq_request_task if TYPE_CHECKING: from nbxmpp.client import Client 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: dict[str, tuple[list[str], Literal["*", "*1", "1*", "1"]]] = { "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"), "pronouns": (["language", "pref", "type", "altid"], "*"), "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: Node, child_name: str) -> list[str]: values: list[str] = [] 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: Node, child_name: str, values: list[str]) -> None: for value in values: node.addChild(child_name, payload=value) def get_multiple_type_value(node: Node, types: list[str]) -> tuple[str, str]: for type_ in types: value = node.getTagData(type_) if value: return type_, value raise ValueError("no value found") def make_parameters(parameters: Parameters) -> Node: parameters_node = Node("parameters") for parameter in parameters.values(): parameters_node.addChild(node=parameter.to_node()) return parameters_node def get_parameters(node: Node) -> Parameters: name = node.getName() definition = PROPERTY_DEFINITION[name] allowed_parameters = definition[0] parameters_node = node.getTag("parameters") if parameters_node is None: return Parameters() parameters: dict[str, Any] = {} 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: Node) -> Parameter: 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) # type: ignore def to_node(self) -> Node: node = Node(self.name) node.addChild(self.type, payload=self.value) return node def copy(self) -> Parameter: return self.__class__(value=self.value) # type: ignore @dataclass class MultiParameter: name: str type: str values: set[str] @classmethod def from_node(cls, node: Node) -> MultiParameter: 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[str] = 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) # type: ignore def to_node(self) -> Node: node = Node(self.name) for value in self.values: node.addChild(self.type, payload=value) return node def copy(self) -> MultiParameter: return self.__class__(values=set(self.values)) # type: ignore @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: Node) -> TzParameter: 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 = Node(self.name) node.addChild(self.value_type, payload=self.value) return node def copy(self) -> TzParameter: return self.__class__(value_type=self.value_type, value=self.value) class Parameters: def __init__( self, parameters: dict[str, Parameter | MultiParameter | TzParameter] | None = None, ) -> None: if parameters is None: parameters = {} self._parameters = parameters def values(self) -> ValuesView[Parameter | MultiParameter | TzParameter]: return self._parameters.values() def get_parameter(self, name: str) -> Parameter | MultiParameter | TzParameter: return self._parameters[name] def get_types(self) -> set[str]: parameter = self._parameters.get("type") if parameter is None: return set() assert isinstance(parameter, TypeParameter) return parameter.values def remove_types(self, types: list[str]) -> None: parameter = self._parameters.get("type") if parameter is None: raise ValueError("no type parameter") assert isinstance(parameter, TypeParameter) for type_ in types: parameter.values.discard(type_) if not parameter.values: self._parameters.pop("type") def add_types(self, types: list[str]) -> None: parameter = self._parameters.get("type") if parameter is None: parameter = TypeParameter(set(types)) self._parameters["type"] = parameter return assert isinstance(parameter, TypeParameter) parameter.values.update(types) def copy(self) -> Parameters: parameters: dict[str, Parameter | MultiParameter | TzParameter] = {} 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: Node) -> UriProperty: 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) # type: ignore def to_node(self) -> Node: node = Node(self.name) if self.parameters: node.addChild(node=make_parameters(self.parameters)) node.addChild("uri", payload=self.value) # type: ignore return node @property def is_empty(self) -> bool: return not self.value def copy(self) -> UriProperty: return self.__class__( value=self.value, parameters=self.parameters.copy() ) # type: ignore @dataclass class TextProperty: name: str value: str parameters: Parameters = field(default_factory=Parameters) @classmethod def from_node(cls, node: Node) -> TelProperty: 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) # type: ignore def to_node(self) -> Node: 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) -> bool: return not self.value def copy(self) -> TextProperty: return self.__class__( value=self.value, parameters=self.parameters.copy() ) # type: ignore @dataclass class TextListProperty: name: str values: list[str] parameters: Parameters = field(default_factory=Parameters) @classmethod def from_node(cls, node: Node) -> TextListProperty: 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) # type: ignore def to_node(self) -> Node: 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) -> bool: return not self.values def copy(self) -> TextListProperty: return self.__class__( values=list(self.values), parameters=self.parameters.copy() ) # type: ignore @dataclass class MultipleValueProperty: name: str value_type: str value: str parameters: Parameters = field(default_factory=Parameters) @classmethod def from_node(cls, node: Node) -> MultipleValueProperty: 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) # type: ignore def to_node(self) -> Node: 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) -> bool: return not self.value def copy(self) -> MultipleValueProperty: return self.__class__( value_type=self.value_type, value=self.value, parameters=self.parameters.copy(), ) # type: ignore @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: Node) -> KindProperty: # type: ignore 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: Node) -> NProperty: 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 = 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) -> bool: return not ( self.surname or self.given or self.additional or self.suffix or self.prefix ) def copy(self) -> NProperty: 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: str | None = None identity: str | None = None parameters: Parameters = field(default_factory=Parameters) @classmethod def from_node(cls, node: Node) -> GenderProperty: 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 = 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) -> bool: return not (self.sex or self.identity) def copy(self) -> GenderProperty: return self.__class__( sex=self.sex, identity=self.identity, parameters=self.parameters.copy() ) @dataclass class PronounsProperty(TextProperty): name: str = field(default="pronouns", init=False) @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: Node) -> AdrProperty: 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 = 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) -> bool: return not ( self.pobox or self.ext or self.street or self.locality or self.region or self.code or self.country ) def copy(self) -> AdrProperty: 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: Node) -> LangProperty: 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 = 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) -> bool: return not self.value def copy(self) -> LangProperty: 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: Node) -> RevProperty: # type: ignore 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 = 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: Node) -> ClientpidmapProperty: 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 = 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) -> bool: return not self.uri def copy(self) -> ClientpidmapProperty: 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, "pronouns": PronounsProperty, "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, } PropertyT = ( SourceProperty | KindProperty | FnProperty | NProperty | NicknameProperty | PhotoProperty | BDayProperty | AnniversaryProperty | GenderProperty | PronounsProperty | AdrProperty | TelProperty | EmailProperty | ImppProperty | LangProperty | TzProperty | GeoProperty | TitleProperty | RoleProperty | LogoProperty | OrgProperty | MemberProperty | RelatedProperty | CategoriesProperty | NoteProperty | ProdidProperty | RevProperty | SoundProperty | UidProperty | ClientpidmapProperty | UrlProperty | KeyProperty | FBurlProperty | CaladruriProperty | CaluriProperty ) def get_property_from_name(name: str, node: Node) -> PropertyT | None: 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 VCardPropertiesT = list[tuple[str | None, PropertyT | list[PropertyT]]] NotSupportedNodesT = dict[str | None, list[Node]] class VCard: def __init__( self, properties: VCardPropertiesT | None = None, not_supported_nodes: NotSupportedNodesT | None = None, ) -> None: if properties is None: properties = [] if not_supported_nodes is None: not_supported_nodes = defaultdict(list) self._properties = properties self._no_supported_nodes = not_supported_nodes @classmethod def from_node(cls, node: Node) -> VCard: properties: VCardPropertiesT = [] not_supported_nodes: NotSupportedNodesT = defaultdict(list) for child in node.getChildren(): if isinstance(child, str): continue child_name = child.getName() if child_name == "group": group_name = child.getAttr("name") if not group_name: continue group_properties: list[PropertyT] = [] for group_child in child.getChildren(): if isinstance(group_child, str): continue group_child_name = group_child.getName() property_ = get_property_from_name(group_child_name, group_child) if property_ is None: not_supported_nodes[group_name].append(group_child) continue group_properties.append(property_) properties.append((group_name, group_properties)) else: property_ = get_property_from_name(child_name, child) if property_ is None: not_supported_nodes[None].append(child) continue properties.append((None, property_)) return cls(properties, not_supported_nodes) def to_node(self) -> Node: vcard = Node(f"{Namespace.VCARD4} vcard") for group, props in self._properties: if group is None: assert not isinstance(props, list) vcard.addChild(node=props.to_node()) else: group_node = Node(group) assert isinstance(props, list) for prop in props: group_node.addChild(node=prop.to_node()) for node in self._no_supported_nodes.get(group, []): group_node.addChild(node=node) vcard.addChild(node=group_node) for node in self._no_supported_nodes.get(None, []): vcard.addChild(node=node) return vcard def get_properties(self) -> list[PropertyT]: properties: list[PropertyT] = [] for group, props in self._properties: if group is None: assert not isinstance(props, list) properties.append(props) else: assert isinstance(props, list) properties.extend(props) return properties def add_property(self, name: str, *args: Any, **kwargs: Any): prop = PROPERTY_CLASSES.get(name)(*args, **kwargs) self._properties.append((None, prop)) return prop def remove_property(self, prop: PropertyT) -> None: 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) -> VCard: properties: VCardPropertiesT = [] for group_name, props in self._properties: if group_name is None: assert not isinstance(props, list) properties.append((None, props.copy())) else: assert isinstance(props, list) 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", "get_access_model": "PubSub", "request_items": "PubSub", } def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [ StanzaHandler( name="message", callback=self._process_pubsub_vcard, ns=Namespace.PUBSUB_EVENT, priority=16, ), ] def _process_pubsub_vcard( self, _client: Client, stanza: Message, properties: MessageProperties ) -> None: if not properties.is_pubsub_event: return assert properties.pubsub_event is not None if properties.pubsub_event.node != Namespace.VCARD4_PUBSUB: return item = properties.pubsub_event.item if item is None: # Retract, Deleted or Purged return try: vcard = _get_vcard(item) except MalformedStanzaError as error: self._log.warning(error) self._log.warning(error.stanza) raise NodeProcessed pubsub_event = properties.pubsub_event._replace(data=vcard) self._log.info("Received VCard4: %s", properties.jid) properties.pubsub_event = pubsub_event @iq_request_task def request_vcard(self, jid: JID | None = 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: VCard, public: bool | None = None): task = yield if public is not None: access_model = "open" if public else "presence" else: result = yield self.get_access_model(Namespace.VCARD4_PUBSUB) access_model = "presence" if is_error(result) else result 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: Node) -> VCard: 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/vcard_avatar.py000066400000000000000000000045141524361513300274430ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING from nbxmpp.const import AvatarState from nbxmpp.const import PresenceType from nbxmpp.modules.base import BaseModule from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Presence from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler from nbxmpp.util import normalize_sha1 if TYPE_CHECKING: from nbxmpp.client import Client class VCardAvatar(BaseModule): def __init__(self, client: Client) -> None: 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: Client, stanza: Presence, properties: PresenceProperties ) -> None: 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", properties.jid) # Empty update element, ignore return if avatar_sha == "": properties.avatar_state = AvatarState.EMPTY self._log.info("%s empty avatar advertised", properties.jid) return # XEP-0153 hashes are hex SHA-1 of the image bytes. Implementations must # accept mixed case and should emit lowercase. normalized_sha = normalize_sha1(avatar_sha) if normalized_sha is None: properties.avatar_state = AvatarState.IGNORE self._log.warning( "%s advertised invalid avatar hash: %r", properties.jid, avatar_sha, ) return properties.avatar_sha = avatar_sha properties.avatar_state = AvatarState.ADVERTISED self._log.info("%s advertises %s", properties.jid, avatar_sha) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/modules/vcard_temp.py000066400000000000000000000104151524361513300271270ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING import hashlib import logging from dataclasses import dataclass from dataclasses import field from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import process_response from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.simplexml import Node from nbxmpp.task import iq_request_task from nbxmpp.util import b64decode from nbxmpp.util import b64encode if TYPE_CHECKING: from nbxmpp.client import Client log = logging.getLogger("nbxmpp.m.vcardtemp") class VCardTemp(BaseModule): def __init__(self, client: Client) -> None: BaseModule.__init__(self, client) self._client = client self.handlers = [] @iq_request_task def request_vcard(self, jid: JID | None = 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: VCard, jid: JID | None = None): _task = yield response = yield _make_vcard_publish(jid, vcard) yield process_response(response) def _make_vcard_request(jid: JID) -> Iq: iq = Iq(typ="get", to=jid) iq.addChild("vCard", namespace=Namespace.VCARD) return iq def _get_vcard_node(response: Node) -> Node: 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: JID, vcard: VCard) -> Iq: 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: Node) -> VCard: 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) -> Node: 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: bytes, type_: str | None = None) -> None: encoded_avatar = b64encode(avatar) if "PHOTO" not in self.data: self.data["PHOTO"] = {} self.data["PHOTO"]["BINVAL"] = encoded_avatar if type_ is not None: self.data["PHOTO"]["TYPE"] = type_ def get_avatar(self) -> tuple[bytes | None, str | None]: try: avatar = self.data["PHOTO"]["BINVAL"] except Exception: return None, None if not avatar: return None, None try: decoded_avatar = b64decode(avatar) except Exception as error: log.warning("Unable to decode avatar: %s", error) return None, None avatar_sha = hashlib.sha1(decoded_avatar).hexdigest() return decoded_avatar, avatar_sha python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/namespaces.py000066400000000000000000000214571524361513300254620ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from dataclasses import dataclass @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:1#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" CHANNEL_BINDING: str = "urn:xmpp:sasl-cb:0" 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" FALLBACK: str = "urn:xmpp:fallback:0" FASTEN: str = "urn:xmpp:fasten:0" 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" HATS: str = "urn:xmpp:hats:0" HATS_LEGACY: str = "xmpp:prosody.im/protocol/hats:1" 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" IBR_TOKEN = "urn:xmpp:ibr-token:0" # noqa: S105 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_RTCP_FB: str = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0" 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" JINGLE_MESSAGE: str = "urn:xmpp:jingle-message: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" MESSAGE_MODERATE: str = "urn:xmpp:message-moderate:0" MESSAGE_MODERATE_1: str = "urn:xmpp:message-moderate:1" MESSAGE_RETRACT: str = "urn:xmpp:message-retract:0" MESSAGE_RETRACT_1: str = "urn:xmpp:message-retract:1" MDS: str = "urn:xmpp:mds:displayed:0" MDS_ASSIST: str = "urn:xmpp:mds:server-assist:0" 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" OCCUPANT_ID: str = "urn:xmpp:occupant-id:0" 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" OPEN_GRAPH: str = "https://ogp.me/ns#" OPENPGP: str = "urn:xmpp:openpgp:0" OPENPGP_PK: str = "urn:xmpp:openpgp:0:public-keys" OPENPGP_SK: str = "urn:xmpp:openpgp:0:secret-key" PARS: str = "urn:xmpp:pars:0" PING: str = "urn:xmpp:ping" PRE_APPROVAL: str = "urn:xmpp:features:pre-approval" 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" REACTIONS: str = "urn:xmpp:reactions:0" REACTIONS_RESTRICTIONS: str = "urn:xmpp:reactions:0:restrictions" RECEIPTS: str = "urn:xmpp:receipts" REGISTER: str = "jabber:iq:register" REGISTER_FEATURE: str = "http://jabber.org/features/iq-register" REPLY: str = "urn:xmpp:reply:0" 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" RDF: str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" RSM: str = "http://jabber.org/protocol/rsm" SASL: str = "urn:ietf:params:xml:ns:xmpp-sasl" SASL2: str = "urn:xmpp:sasl:2" SEARCH: str = "jabber:iq:search" SECLABEL: str = "urn:xmpp:sec-label:0" SECLABEL_CATALOG: str = "urn:xmpp:sec-label:catalog:2" SERVICE_DISCOVERY: str = "urn:xmpp:extdisco: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" TIME: 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" XRD: str = "http://docs.oasis-open.org/ns/xri/xrd-1.0" Namespace: _Namespaces = _Namespaces() python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/old_dispatcher.py000066400000000000000000000666511524361513300263340ustar00rootroot00000000000000# Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov # modified by Dimitur Kirov # # SPDX-License-Identifier: GPL-2.0-or-later """ Main xmpp decision making logic. Provides library with methods to assign different handlers to different XMPP stanzas and namespaces """ import inspect import logging import sys import uuid from xml.parsers.expat import ExpatError from nbxmpp.modules.activity import Activity from nbxmpp.modules.adhoc import AdHoc from nbxmpp.modules.annotations import Annotations from nbxmpp.modules.attention import Attention from nbxmpp.modules.blocking import Blocking from nbxmpp.modules.captcha import Captcha from nbxmpp.modules.chat_markers import ChatMarkers from nbxmpp.modules.chatstates import Chatstates from nbxmpp.modules.correction import Correction from nbxmpp.modules.delay import Delay from nbxmpp.modules.discovery import Discovery from nbxmpp.modules.eme import EME from nbxmpp.modules.entity_caps import EntityCaps from nbxmpp.modules.http_auth import HTTPAuth from nbxmpp.modules.http_upload import HTTPUpload from nbxmpp.modules.ibb import IBB from nbxmpp.modules.idle import Idle from nbxmpp.modules.iq import BaseIq from nbxmpp.modules.location import Location from nbxmpp.modules.message import BaseMessage from nbxmpp.modules.misc import unwrap_carbon from nbxmpp.modules.misc import unwrap_mam from nbxmpp.modules.mood import Mood from nbxmpp.modules.muc import MUC from nbxmpp.modules.muclumbus import Muclumbus from nbxmpp.modules.nickname import Nickname from nbxmpp.modules.omemo import OMEMO from nbxmpp.modules.oob import OOB from nbxmpp.modules.openpgp import OpenPGP from nbxmpp.modules.pgplegacy import PGPLegacy from nbxmpp.modules.presence import BasePresence from nbxmpp.modules.pubsub import PubSub from nbxmpp.modules.receipts import Receipts from nbxmpp.modules.register import Register from nbxmpp.modules.security_labels import SecurityLabels from nbxmpp.modules.software_version import SoftwareVersion from nbxmpp.modules.tune import Tune from nbxmpp.modules.user_avatar import UserAvatar from nbxmpp.modules.vcard_avatar import VCardAvatar from nbxmpp.namespaces import Namespace from nbxmpp.plugin import PlugIn from nbxmpp.protocol import ERR_FEATURE_NOT_IMPLEMENTED from nbxmpp.protocol import Error from nbxmpp.protocol import InvalidFrom from nbxmpp.protocol import InvalidJid from nbxmpp.protocol import InvalidStanza from nbxmpp.protocol import Iq from nbxmpp.protocol import Message from nbxmpp.protocol import Node from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Presence from nbxmpp.protocol import Protocol from nbxmpp.simplexml import NodeBuilder from nbxmpp.util import get_properties_struct from nbxmpp.util import INVALID_XML_RX 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, ] 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 INVALID_XML_RX.sub("\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.stream_error) 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/plugin.py000066400000000000000000000060141524361513300246310ustar00rootroot00000000000000# Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov # # SPDX-License-Identifier: GPL-2.0-or-later """ 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/precis.py000066400000000000000000000007601524361513300246220ustar00rootroot00000000000000from precis_i18n import get_profile _localpart_disallowed_chars = set("\"&'/:<>@") def enforce_precis_username(localpart: str) -> str: if _localpart_disallowed_chars & set(localpart): raise ValueError("Input contains prohibited codepoint: %s" % localpart) username = get_profile("UsernameCaseMapped") return username.enforce(localpart) def enforce_precis_opaque(resourcepart: str) -> str: opaque = get_profile("OpaqueString") return opaque.enforce(resourcepart) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/protocol.py000066400000000000000000002322041524361513300251760ustar00rootroot00000000000000# Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov # # SPDX-License-Identifier: GPL-2.0-or-later """ 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 """ from __future__ import annotations from typing import Any from typing import Literal from typing import TYPE_CHECKING import functools import hashlib import os import sqlite3 import time import warnings from base64 import b64encode from collections.abc import Iterable from dataclasses import asdict from dataclasses import dataclass from dataclasses import fields import idna from gi.repository import Gio from gi.repository import GLib from nbxmpp.namespaces import Namespace from nbxmpp.precis import enforce_precis_opaque from nbxmpp.precis import enforce_precis_username from nbxmpp.simplexml import Node from nbxmpp.stringprep import nodeprep from nbxmpp.stringprep import resourceprep from nbxmpp.xmppiri import clean_iri from nbxmpp.xmppiri import escape_ifragment from nbxmpp.xmppiri import escape_inode from nbxmpp.xmppiri import escape_ires from nbxmpp.xmppiri import escape_ivalue from nbxmpp.xmppiri import validate_ikey from nbxmpp.xmppiri import validate_querytype if TYPE_CHECKING: from nbxmpp.structs import MucSubject from nbxmpp.structs import OpenGraphData def ascii_upper(s: str) -> str: return s.upper() SASL_AUTH_MECHS = [ "SCRAM-SHA-512-PLUS", "SCRAM-SHA-256-PLUS", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-512", "SCRAM-SHA-256", "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: dict[str, tuple[str, str]] = { "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": ( "modify", "The sender has sent XML that is malformed or that cannot be processed.", ), "urn:ietf:params:xml:ns:xmpp-stanzas conflict": ( "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": ( "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": ( "auth", "The requesting entity does not possess the required permissions to perform the action.", ), "urn:ietf:params:xml:ns:xmpp-stanzas gone": ( "modify", "The recipient or server can no longer be contacted at this address.", ), "urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error": ( "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": ( "cancel", "The addressed JID or item requested cannot be found.", ), "urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed": ( "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": ( "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": ( "cancel", "The recipient or server does not allow any entity to perform the action.", ), "urn:ietf:params:xml:ns:xmpp-stanzas not-authorized": ( "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": ( "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": ( "wait", "The intended recipient is temporarily unavailable.", ), "urn:ietf:params:xml:ns:xmpp-stanzas redirect": ( "modify", "The recipient or server is redirecting requests for this information to another entity.", ), "urn:ietf:params:xml:ns:xmpp-stanzas registration-required": ( "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": ( "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": ( "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": ( "wait", "The server or recipient lacks the system resources necessary to service the request.", ), "urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable": ( "cancel", "The server or recipient does not currently provide the requested service.", ), "urn:ietf:params:xml:ns:xmpp-stanzas subscription-required": ( "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": ( "", "Undefined Condition", ), "urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request": ( "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": ( "", "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.", ), } _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: Protocol) -> bool: """ Return true if the node is a positive reply """ return node and node.getType() == "result" def isErrorNode(node: Protocol) -> bool: """ Return true if the node is a negative reply """ return node and node.getType() == "error" def isMucPM(message: Message) -> bool: 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: str) -> None: warnings.warn(message, DeprecationWarning, stacklevel=1) def split_jid_string(jid_string: str) -> tuple[str | None, str, str | None]: # 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 localpart, domainpart, resourcepart @functools.cache def validate_localpart(localpart: str) -> str: if not localpart or len(localpart.encode()) > 1023: raise LocalpartByteLimit if os.environ.get("NBXMPP_ENFORCE_PRECIS") is None: try: return nodeprep(localpart) except Exception: try: return enforce_precis_username(localpart) except Exception: raise LocalpartNotAllowedChar try: return enforce_precis_username(localpart) except Exception: raise LocalpartNotAllowedChar @functools.cache def validate_resourcepart(resourcepart: str) -> str: if not resourcepart or len(resourcepart.encode()) > 1023: raise ResourcepartByteLimit if os.environ.get("NBXMPP_ENFORCE_PRECIS") is None: try: return resourceprep(resourcepart) except Exception: try: return enforce_precis_opaque(resourcepart) except Exception: raise ResourcepartNotAllowedChar try: return enforce_precis_opaque(resourcepart) except Exception: raise ResourcepartNotAllowedChar @functools.cache def validate_domainpart(domainpart: str) -> str: 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: domainpart = idna2008_prep(domainpart) except Exception: raise DomainpartNotAllowedChar return domainpart @functools.cache def idna2008_prep(domain: str, to_ascii: bool = False) -> str: """ Prepare with UTS46 case mapping to stay compatibel with the IDNA2003 mapping. Further try to encode the domain to catch illegal domains. Only return the case mapped domain because on the XMPP wire,UTF8 domains are fine. """ domain = idna.uts46_remap(domain) encoded_domain = idna.encode(domain) if to_ascii: return encoded_domain.decode() return domain @functools.cache def escape_localpart(localpart: str) -> str: # 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.cache def unescape_localpart(localpart: str) -> str: 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 @dataclass(frozen=True, kw_only=True) class JID: localpart: str | None domain: str resource: str | None def __init__( self, localpart: str | None, domain: str, resource: str | None, ): if localpart is not None: localpart = validate_localpart(localpart) object.__setattr__(self, "localpart", localpart) domain = validate_domainpart(domain) object.__setattr__(self, "domain", domain) if resource is not None: resource = validate_resourcepart(resource) object.__setattr__(self, "resource", resource) @classmethod @functools.cache def from_string(cls, jid_string: str, force_bare: bool = False) -> JID: localpart, domainpart, resourcepart = split_jid_string(jid_string) if force_bare: resourcepart = None try: return cls(localpart=localpart, domain=domainpart, resource=resourcepart) except Exception as error: raise InvalidJid('Unable to parse "%s"' % jid_string) from error @classmethod @functools.cache def from_user_input(cls, user_input: str, escaped: bool = False) -> JID: # 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 try: return cls(localpart=localpart, domain=domainpart, resource=None) except Exception as error: raise InvalidJid('Unable to parse "%s"' % user_input) from error @classmethod @functools.cache def from_iri(cls, iri_str: str, *, force_bare: bool = False) -> JID: try: iri_str = clean_iri(iri_str) except ValueError as error: raise InvalidJid('Unable to parse "%s"' % iri_str) from error localpart, domainpart, resourcepart = split_jid_string(iri_str) if localpart is not None: localpart = GLib.Uri.unescape_string(localpart) if force_bare: resourcepart = None if resourcepart is not None: resourcepart = GLib.Uri.unescape_string(resourcepart) try: return cls(localpart=localpart, domain=domainpart, resource=resourcepart) except Exception as error: raise InvalidJid('Unable to parse "%s"' % iri_str) from error def __str__(self) -> str: 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 __conform__(self, protocol: sqlite3.PrepareProtocol | Any) -> str: if protocol is sqlite3.PrepareProtocol: return str(self) raise ValueError def __hash__(self) -> int: return hash(str(self)) def __eq__(self, other: object) -> bool: if isinstance(other, str): try: return JID.from_string(other) == self except Exception: return False if isinstance(other, JID): return ( self.localpart == other.localpart and self.domain == other.domain and self.resource == other.resource ) return False def __ne__(self, other: object) -> bool: return not self.__eq__(other) def domain_to_ascii(self) -> str: return idna2008_prep(self.domain, to_ascii=True) @property def bare(self) -> str: if self.localpart is not None: return f"{self.localpart}@{self.domain}" return self.domain @property def is_bare(self) -> bool: return self.resource is None def new_as_bare(self) -> JID: if self.resource is None: return self return JID.from_string(self.bare) def bare_match(self, other: str | JID) -> bool: if isinstance(other, str): other = JID.from_string(other) return self.bare == other.bare @property def is_domain(self) -> bool: return self.localpart is None and self.resource is None @property def is_full(self) -> bool: return self.localpart is not None and self.resource is not None def new_with(self, **kwargs: Any) -> JID: new = asdict(self) new.update(kwargs) return JID(**new) def to_user_string(self, show_punycode: bool = True) -> str: 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 to_iri( self, query: str | tuple[str, list[tuple[str, str]]] | None = None, fragment: str | None = None, ) -> str: if self.localpart: inode = escape_inode(self.localpart) ipathxmpp = f"{inode}@{self.domain}" else: ipathxmpp = f"{self.domain}" if self.resource is not None: ires = escape_ires(self.resource) ipathxmpp = f"{ipathxmpp}/{ires}" iri = f"xmpp:{ipathxmpp}" if query is not None: if isinstance(query, str): querytype = query queryparams = None else: querytype, queryparams = query iquerytype = validate_querytype(querytype) iri += f"?{iquerytype}" if queryparams is not None: for ikey, ivalue in queryparams: ivalue = escape_ivalue(ivalue) ikey = validate_ikey(ikey) iri += f";{ikey}={ivalue}" if fragment is not None: ifragment = escape_ifragment(fragment) iri += f"#{ifragment}" return iri def copy(self) -> JID: deprecation_warning("copy() is not needed, JID is immutable") return self class StreamErrorNode(Node): def __init__(self, node: Protocol) -> None: Node.__init__(self, node=node) self._text: dict[str | None, str] = {} 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) -> str | None: 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: str | None = None) -> str: 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: str | None = None, to: JID | str | None = None, typ: str | None = None, frm: JID | str | None = None, attrs: dict[str, Any] | None = None, payload: list[Node | str] | None = None, timestamp: str | None = None, xmlns: str | None = None, node: Node | bytes | str | None = None, ) -> 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: str | None = 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) -> bool: return self.getAttr("type") == "error" def isResult(self) -> bool: return self.getAttr("type") == "result" def getTo(self) -> JID | None: """ Return value of the 'to' attribute """ try: return self["to"] except Exception: pass return None def getFrom(self) -> JID | None: """ Return value of the 'from' attribute """ try: return self["from"] except Exception: pass return None def getTimestamp(self) -> str: """ 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) -> str: """ 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) -> str | None: """ Return the value of the 'id' attribute """ return self.getAttr("id") def setTo(self, val: str | JID): """ Set the value of the 'to' attribute """ if not isinstance(val, JID): val = JID.from_string(val) self.setAttr("to", val) def getType(self) -> str | None: """ Return the value of the 'type' attribute """ return self.getAttr("type") def setFrom(self, val: str | JID) -> None: """ 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: str) -> None: """ Set the value of the 'type' attribute """ self.setAttr("type", val) def setID(self, val: str) -> None: """ Set the value of the 'id' attribute """ self.setAttr("id", val) def getError(self) -> str | None: """ 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) -> str | None: 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) -> str | None: 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) -> str | None: """ 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 getErrorType(self) -> str | None: """ Return the error type. """ return self.getTagAttr("error", "type") def getStatusConditions(self, as_code: bool = False) -> list[str]: """ Return the status conditions list as defined in XEP-0306. """ result: set[str] = 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: ErrorNode | str) -> None: """ Set the error-conditions """ if isinstance(error, str): error = ErrorNode(error) self.setType("error") self.addChild(node=error) def setTimestamp(self, val: str | None = None) -> 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) -> list[str]: """ Return the list of namespaces to which belongs the direct childs of element """ props: list[str] = [] for child in self.getChildren(): prop = child.getNamespace() if prop not in props: props.append(prop) return props def getTag( self, name: str, attrs: dict[str, Any] | None = None, namespace: str | None = None, protocol: bool = False, ) -> Node | None: """ 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: str, val: str | JID) -> None: """ Set the item 'item' to the value 'val' """ if item in ["to", "from"]: if not isinstance(val, JID): val = JID.from_string(val) self.setAttr(item, val) class Message(Protocol): """ XMPP Message stanza - "push" mechanism """ def __init__( self, to: JID | str | None = None, body: str | None = None, xhtml: str | None = None, typ: Literal["chat", "groupchat", "normal", "headline", "error"] | None = None, subject: MucSubject | None = None, attrs: dict[str, Any] | None = None, frm: JID | str | None = None, payload: list[Node | str] | None = None, timestamp: str | None = None, xmlns: str = Namespace.CLIENT, node: Node | str | None = None, ) -> 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) -> str | None: """ Return text of the message """ return self.getTagData("body") def getXHTML(self) -> Node | None: return self.getTag("html", namespace=Namespace.XHTML_IM) def getSubject(self) -> str | None: """ Return subject of the message """ return self.getTagData("subject") def getThread(self) -> str | None: """ Return thread of the message """ return self.getTagData("thread") def getOriginID(self) -> str | None: """ Return origin-id of the message """ return self.getTagAttr("origin-id", namespace=Namespace.SID, attr="id") def getStanzaIDAttrs(self) -> tuple[str | None, str | None]: """ 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: str) -> None: """ Set the text of the message""" self.setTagData("body", val) def setXHTML(self, body: str | Node, add: bool = False) -> None: 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: MucSubject | None) -> None: """ Set the subject of the message """ self.setTagData("subject", val) def setThread(self, val: str) -> None: """ Set the thread of the message """ self.setTagData("thread", val) def setOriginID(self, val: str) -> None: """ Sets the origin-id of the message """ self.setTag("origin-id", namespace=Namespace.SID, attrs={"id": val}) def buildReply(self, text: str | None = None) -> Message: """ 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) -> list[str | None]: """ Return the status code of the message (for groupchat config change) """ attrs: list[str | None] = [] for xtag in self.getTags("x"): for child in xtag.getTags("status"): attrs.append(child.getAttr("code")) return attrs def setMarker(self, type_: str, id_: str) -> None: self.setTag(type_, namespace=Namespace.CHATMARKERS, attrs={"id": id_}) def setMarkable(self) -> None: self.setTag("markable", namespace=Namespace.CHATMARKERS) def setMdsAssist(self, stanza_id: str, by: JID) -> None: displayed = self.setTag("displayed", namespace=Namespace.MDS) displayed.addChild( "stanza-id", namespace=Namespace.SID, attrs={"id": stanza_id, "by": str(by)} ) def setReceiptRequest(self) -> None: self.setTag("request", namespace=Namespace.RECEIPTS) def setReceiptReceived(self, id_: str) -> None: self.setTag("received", namespace=Namespace.RECEIPTS, attrs={"id": id_}) def setReply( self, recipient_jid: str, reply_to_id: str, fallback_start: int, fallback_end: int, ) -> None: self.setTag( "reply", namespace=Namespace.REPLY, attrs={"id": reply_to_id, "to": recipient_jid}, ) fallback_tag = self.setTag( "fallback", namespace=Namespace.FALLBACK, attrs={"for": Namespace.REPLY} ) fallback_tag.addChild( "body", attrs={"start": str(fallback_start), "end": str(fallback_end)} ) def setOOB(self, url: str, desc: str | None = None) -> 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_: str) -> None: self.setTag("replace", namespace=Namespace.CORRECT, attrs={"id": id_}) def setAttention(self) -> None: self.setTag("attention", namespace=Namespace.ATTENTION) def setHint(self, hint: str) -> None: self.setTag(hint, namespace=Namespace.HINTS) def setReactions(self, target_id: str, emojis: Iterable[str]) -> None: reactions = self.addChild( "reactions", namespace=Namespace.REACTIONS, attrs={"id": target_id} ) for e in emojis: reactions.addChild("reaction", namespace=Namespace.REACTIONS, payload=[e]) def getReactions(self) -> tuple[str, set[str]] | None: reactions = self.getTag("reactions", namespace=Namespace.REACTIONS) if not reactions: return None react_to = reactions.getAttr("id") if not react_to: return None tags = reactions.getTags("reaction", namespace=Namespace.REACTIONS) # strip() in case clients surround emojis with whitespace return react_to, {t.getData().strip() for t in tags} def setRetracted(self, target_id: str, fallback_text: str | None = None) -> None: retract = self.addChild("retract", namespace=Namespace.MESSAGE_RETRACT_1) retract.setAttr("id", target_id) if not fallback_text: return self.setTag( "fallback", namespace=Namespace.FALLBACK, attrs={"for": Namespace.MESSAGE_RETRACT_1}, ) self.setBody(fallback_text) def addOpenGraph(self, about: str, data: OpenGraphData) -> None: """ Add opengraph data (=link preview) to this message. :param about: The URL related to the opengraph data :param data: The opengraph data """ description = self.addChild( "Description", attrs={ "xmlns:rdf": Namespace.RDF, "rdf:about": about, }, namespace=Namespace.RDF, ) for field in fields(data): tag = field.name value = getattr(data, tag, None) if value: el = description.addChild(tag, namespace=Namespace.OPEN_GRAPH) el.setData(value) class Presence(Protocol): def __init__( self, to: JID | str | None = None, typ: Literal["probe", "unavailable"] | None = None, priority: int | None = None, show: Literal["chat", "away", "xa", "dnd"] | None = None, status: str | None = None, attrs: dict[str, Any] | None = None, frm: JID | None = None, timestamp: str | None = None, payload: list[Node | str] | None = None, xmlns: str = Namespace.CLIENT, node: Node | str | None = None, ) -> 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) -> int: """ Return the priority of the message """ return self.getTagData("priority") def getShow(self) -> str | None: """ Return the show value of the message """ return self.getTagData("show") def getStatus(self) -> str: """ Return the status string of the message """ return self.getTagData("status") or "" def setPriority(self, val: int | None) -> None: """ Set the priority of the message """ self.setTagData("priority", val) def setShow(self, val: str) -> None: """ 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: str) -> None: """ Set the status string of the message """ self.setTagData("status", val) def _muc_getItemAttr(self, tag: str, attr: str): 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: str, attr: str): 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: str | None = None, queryNS: str | None = None, # noqa: N803 attrs: dict[str, str] | None = None, to: JID | str | None = None, frm: JID | str | None = None, payload: list[Node | str] | Node | None = None, xmlns: str = Namespace.CLIENT, node: Node | None = None, ) -> 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) -> Node | None: """ 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) -> str | None: """ Return the namespace of the 'query' child element """ tag = self.getQuery() if tag: return tag.getNamespace() return None def getQuerynode(self) -> str | None: """ Return the 'node' attribute value of the 'query' child element """ tag = self.getQuery() if tag: return tag.getAttr("node") return None def getQueryChildren(self) -> list[Node] | None: """ Return the 'query' child element child nodes """ tag = self.getQuery() if tag: return tag.getChildren() return None def getQueryChild(self, name: str | None = None) -> Node | 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: str | None = None) -> Node: """ 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: str) -> None: """ Set the namespace of the 'query' child element """ self.setQuery().setNamespace(namespace) def setQueryPayload(self, payload: list[Node | str]): """ Set the 'query' child element payload """ self.setQuery().setPayload(payload) def setQuerynode(self, node: str) -> None: """ Set the 'node' attribute value of the 'query' child element """ self.setQuery().setAttr("node", node) def buildReply(self, typ: str) -> Iq: """ 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: str) -> Iq: 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: str = Namespace.HASHES) -> None: Node.__init__(self, None, {}, [], None, None, False, None) self.setNamespace(nsp) self.setName("hash") def calculateHash(self, algo: str, file_string: str | bytes) -> str | None: """ 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_: str, algo: str) -> None: 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: str = Namespace.HASHES_2) -> None: Node.__init__(self, None, {}, [], None, None, False, None) self.setNamespace(nsp) self.setName("hash") def calculateHash(self, algo: str, file_string: str | bytes) -> str | None: """ 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_: str, algo: str) -> None: self.setAttr("algo", algo) self.setData(hash_) class BindRequest(Iq): def __init__(self, resource: str | None) -> None: if resource is None: res = resource else: res = Node("resource", payload=resource) Iq.__init__(self, typ="set") self.addChild(node=Node("bind", {"xmlns": Namespace.BIND}, payload=res)) class TLSRequest(Node): def __init__(self) -> None: Node.__init__(self, tag="starttls", attrs={"xmlns": Namespace.TLS}) class SessionRequest(Iq): def __init__(self) -> None: Iq.__init__(self, typ="set") self.addChild(node=Node("session", attrs={"xmlns": Namespace.SESSION})) class StreamHeader(Node): def __init__(self, domain: str, lang: str | None = None) -> 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: str, lang: str | None = None) -> 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) -> None: Node.__init__(self, tag="close", attrs={"xmlns": Namespace.FRAMING}) class Features(Node): def __init__(self, node: Node) -> None: Node.__init__(self, node=node) def has_starttls(self) -> tuple[bool, bool]: 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) -> bool: return self.getTag("mechanisms", namespace=Namespace.SASL) is not None def has_sasl_2(self) -> bool: return self.getTag("authentication", namespace=Namespace.SASL2) is not None def get_mechs(self) -> set[str]: mechanisms = self.getTag("authentication", namespace=Namespace.SASL2) if mechanisms is None: mechanisms = self.getTag("mechanisms", namespace=Namespace.SASL) if mechanisms is None: return set() mechanisms = mechanisms.getTags("mechanism") return {mech.getData() for mech in mechanisms} def get_domain_based_name(self) -> str | None: hostname = self.getTag("hostname", namespace=Namespace.DOMAIN_BASED_NAME) if hostname is not None: return hostname.getData() return None def has_bind(self) -> bool: return self.getTag("bind", namespace=Namespace.BIND) is not None def session_required(self) -> bool: 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) -> bool: return self.getTag("sm", namespace=Namespace.STREAM_MGMT) is not None def has_roster_version(self) -> bool: return self.getTag("ver", namespace=Namespace.ROSTER_VER) is not None def has_pre_approval(self) -> bool: return self.getTag("sub", namespace=Namespace.PRE_APPROVAL) is not None def has_register(self) -> bool: return self.getTag("register", namespace=Namespace.REGISTER_FEATURE) is not None def has_ibr_token(self) -> bool: return self.getTag("register", namespace=Namespace.IBR_TOKEN) is not None def has_anonymous(self) -> bool: return "ANONYMOUS" in self.get_mechs() def get_channel_binding_type(self) -> Gio.TlsChannelBindingType | None: sasl_cb = self.getTag( "sasl-channel-binding", namespace=Namespace.CHANNEL_BINDING ) if sasl_cb is None: return None exporter = sasl_cb.getTag("channel-binding", attrs={"type": "tls-exporter"}) if exporter is not None: return Gio.TlsChannelBindingType.EXPORTER server_end_point = sasl_cb.getTag( "channel-binding", attrs={"type": "tls-server-end-point"} ) if server_end_point is not None: return Gio.TlsChannelBindingType.SERVER_END_POINT return None 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: str, typ: str | None = None, text: str | None = None ) -> None: """ Mandatory parameter: name - name of error condition. Optional parameters: typ, text. Used for backwards compartibility with older jabber protocol. """ if name in ERRORS: type_, txt = ERRORS[name] ns = name.split()[0] else: ns, type_, txt = Namespace.STANZAS, "cancel", "" if typ: type_ = typ if text: txt = text Node.__init__(self, "error", {}, [Node(name)]) if type_: self.setAttr("type", type_) else: self.setName("stream:error") if txt: self.addChild(node=Node(ns + " text", {}, [txt])) class Error(Protocol): """ Used to quickly transform received stanza into error reply """ def __init__(self, node: Protocol, error: ErrorNode | str, reply: int = 1) -> None: """ 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 node.getType() == "error": raise ValueError("Can’t create error reply from error") if reply: Protocol.__init__(self, to=node.getFrom(), node=node) self.delAttr("from") else: Protocol.__init__(self, node=node) self.setError(error) 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: str | None = None, value=None, typ: str | None = None, required: int = 0, desc: str | None = None, options=None, node=None, ) -> 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: int = 1) -> None: """ Change the state of the 'required' flag """ if req: self.setTag("required") else: try: self.delChild("required") except ValueError: return def isRequired(self) -> bool: """ Return in this field a required one """ return self.getTag("required") def setDesc(self, desc: str | None) -> None: """ Set the description of this field """ self.setTagData("desc", desc) def getDesc(self) -> str | None: """ Return the description of this field """ return self.getTagData("desc") def setValue(self, val) -> None: """ Set the value of this field """ self.setTagData("value", val) def getValue(self): return self.getTagData("value") def setValues(self, lst) -> None: """ 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) -> None: """ Add one more value to this field. Used in 'get' iq's or such """ self.addChild("value", {}, [val]) def getValues(self) -> list[str]: """ Return the list of values associated with this field """ ret: list[str] = [] 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: list[str]) -> None: """ 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: list[str] | str) -> None: """ 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: str) -> None: """ 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: str) -> None: """ 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: str | None = None, data=None, title: str | None = None, node: Node | None = None, ) -> 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: 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) -> None: """ 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: str) -> None: """ 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: str) -> None: """ Set the instructions of dataform """ self.setTagData("instructions", text) def addInstructions(self, text: str) -> None: """ Add one more instruction to the dataform """ self.addChild("instructions", {}, [text]) def getField(self, name: str) -> Node | None: """ Return the datafield object with name 'name' (if exists) """ return self.getTag("field", attrs={"var": name}) def setField(self, name: str): """ 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: str): """ 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: str, val): """ Simple dictionary interface for setting datafields values by their names """ return self.setField(name).setValue(val) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/py.typed000066400000000000000000000000001524361513300244450ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/sasl.py000066400000000000000000000373371524361513300243110ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import TYPE_CHECKING import binascii import hashlib import hmac import logging import os from hashlib import pbkdf2_hmac from gi.repository import Gio from nbxmpp.const import StreamState from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Node from nbxmpp.protocol import Protocol from nbxmpp.protocol import SASL_AUTH_MECHS from nbxmpp.protocol import SASL_ERROR_CONDITIONS from nbxmpp.stringprep import saslprep from nbxmpp.structs import ChannelBindingData from nbxmpp.util import b64decode from nbxmpp.util import b64encode from nbxmpp.util import LogAdapter if TYPE_CHECKING: from nbxmpp.client import Client from nbxmpp.protocol import Features log = logging.getLogger("nbxmpp.sasl") try: gssapi = __import__("gssapi") gssapi_error = None except (ImportError, OSError) as error: gssapi_error = error GSSAPI_ERROR = gssapi_error class SASL: """ Implements SASL authentication. """ def __init__(self, client: Client) -> None: self._client = client self._password: str | None = None self._mechanism_classes = { "ANONYMOUS": ANONYMOUS, "PLAIN": PLAIN, "EXTERNAL": EXTERNAL, "GSSAPI": GSSAPI, "SCRAM-SHA-1": SCRAM_SHA_1, "SCRAM-SHA-1-PLUS": SCRAM_SHA_1_PLUS, "SCRAM-SHA-256": SCRAM_SHA_256, "SCRAM-SHA-256-PLUS": SCRAM_SHA_256_PLUS, "SCRAM-SHA-512": SCRAM_SHA_512, "SCRAM-SHA-512-PLUS": SCRAM_SHA_512_PLUS, } self._allowed_mechs: set[str] | None = None self._enabled_mechs: set[str] | None = None self._sasl_ns: str | None = None self._mechanism: BaseMechanism | None = None self._error: tuple[str | None, str | None] | None = None self._log = LogAdapter(log, {"context": client.log_context}) @property def error(self) -> tuple[str | None, str | None] | None: return self._error def is_sasl2(self) -> bool: assert self._sasl_ns is not None return self._sasl_ns == Namespace.SASL2 def set_password(self, password: str | None): self._password = password @property def password(self) -> str | None: return self._password def delegate(self, stanza: Protocol) -> None: if stanza.getNamespace() != self._sasl_ns: 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 _get_channel_binding_data( self, features: Features ) -> ChannelBindingData | None: if self._client.tls_version != Gio.TlsProtocolVersion.TLS_1_3: return None binding_type = features.get_channel_binding_type() if binding_type is None: return None channel_binding_data = self._client.get_channel_binding_data(binding_type) if channel_binding_data is None: return None return ChannelBindingData(binding_type, channel_binding_data) def start_auth(self, features: Features) -> None: self._mechanism = None self._allowed_mechs = self._client.mechs self._enabled_mechs = self._allowed_mechs self._sasl_ns = Namespace.SASL if features.has_sasl_2(): self._sasl_ns = Namespace.SASL2 self._log.info("Using %s", self._sasl_ns) self._log.info("Allowed mechanisms: %s", self._allowed_mechs) self._error = None channel_binding_data = None # Segfaults see https://gitlab.gnome.org/GNOME/pygobject/-/issues/603 # So for now channel binding is deactivated # channel_binding_data = self._get_channel_binding_data(features) if channel_binding_data is None: self._enabled_mechs.discard("SCRAM-SHA-1-PLUS") self._enabled_mechs.discard("SCRAM-SHA-256-PLUS") self._enabled_mechs.discard("SCRAM-SHA-512-PLUS") if GSSAPI_ERROR is not None: log.debug("Discard GSSAPI because of error: %s", GSSAPI_ERROR) self._enabled_mechs.discard("GSSAPI") feature_mechs = features.get_mechs() self._log.info("Enabled mechanisms: %s", self._enabled_mechs) self._log.info("Server mechanisms: %s", feature_mechs) available_mechs = feature_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.startswith(("SCRAM", "PLAIN")): if not self._password: self._on_sasl_finished(False, "no-password") return mech_class = self._mechanism_classes[chosen_mechanism] self._mechanism = mech_class( self._client.username, self._password, domain_based_name or self._client.domain, ) if isinstance(self._mechanism, SCRAM) and channel_binding_data is not None: self._mechanism.set_channel_binding_data(channel_binding_data) try: self._send_initiate() except AuthFail as error: self._log.error(error) self._abort_auth() return def _send_initiate(self) -> None: assert self._mechanism is not None data = self._mechanism.get_initiate_data() nonza = get_initiate_nonza(self._sasl_ns, self._mechanism.name, data) self._client.send_nonza(nonza) def _on_challenge(self, stanza: Protocol) -> None: assert self._mechanism is not None try: data = self._mechanism.get_response_data(stanza.getData()) except AttributeError: self._log.info("Mechanism has no response method") self._abort_auth() return except AuthFail as error: self._log.error(error) self._abort_auth() return nonza = get_response_nonza(self._sasl_ns, data) self._client.send_nonza(nonza) def _on_success(self, stanza: Protocol) -> None: self._log.info("Successfully authenticated with remote server") data = get_success_data(stanza, self._sasl_ns) try: self._mechanism.validate_success_data(data) except Exception as error: self._log.error("Unable to validate success data: %s", error) self._abort_auth() return self._log.info("Validated success data") self._on_sasl_finished(True, None, None) def _on_failure(self, stanza: Protocol) -> None: 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: str = "malformed-request", text: str | None = None ) -> None: node = Node("abort", attrs={"xmlns": self._sasl_ns}) self._client.send_nonza(node) self._on_sasl_finished(False, reason, text) def _on_sasl_finished( self, successful: bool, reason: str | None, text: str | None = None ) -> None: if not successful: self._error = (reason, text) self._client.set_state(StreamState.AUTH_FAILED) else: self._client.set_state(StreamState.AUTH_SUCCESSFUL) def get_initiate_nonza(ns: str, mechanism: str, data: str | None) -> Node: if ns == Namespace.SASL: node = Node("auth", attrs={"xmlns": ns, "mechanism": mechanism}) if data is not None: node.setData(data) else: node = Node("authenticate", attrs={"xmlns": ns, "mechanism": mechanism}) if data is not None: node.setTagData("initial-response", data) return node def get_response_nonza(ns: str, data: str) -> Node: return Node("response", attrs={"xmlns": ns}, payload=[data]) def get_success_data(stanza: Protocol, ns: str) -> str | None: if ns == Namespace.SASL2: return stanza.getTagData("additional-data") return stanza.getData() class BaseMechanism: name: str def __init__(self, username: str, password: str, domain: str) -> None: self._username = username self._password = password self._domain = domain def get_initiate_data(self) -> str | None: raise NotImplementedError def get_response_data(self, data: str) -> str: raise NotImplementedError def validate_success_data(self, _data: str) -> None: return None class PLAIN(BaseMechanism): name = "PLAIN" def get_initiate_data(self) -> str: try: password = saslprep(self._password) except Exception: password = "" return b64encode("\x00%s\x00%s" % (self._username, password)) class EXTERNAL(BaseMechanism): name = "EXTERNAL" def get_initiate_data(self) -> str: return b64encode("%s@%s" % (self._username, self._domain)) class ANONYMOUS(BaseMechanism): name = "ANONYMOUS" def get_initiate_data(self) -> None: return None class GSSAPI(BaseMechanism): # See https://tools.ietf.org/html/rfc4752#section-3.1 name = "GSSAPI" def get_initiate_data(self) -> str: service = gssapi.Name( "xmpp@%s" % self._domain, 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) return b64encode(token) def get_response_data(self, data: str | bytes) -> str: byte_data = b64decode(data) try: if not self.ctx.complete: output_token = self.ctx.step(byte_data) else: _result = self.ctx.unwrap(byte_data) # 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) return b64encode(output_token) class SCRAM(BaseMechanism): name = "" _hash_method = "" def __init__(self, *args: Any, **kwargs: Any) -> None: BaseMechanism.__init__(self, *args, **kwargs) self._channel_binding_data: ChannelBindingData | None = None self._gs2_header = "n,," self._client_nonce = "%x" % int(binascii.hexlify(os.urandom(24)), 16) self._client_first_message_bare = None self._server_signature = None def set_channel_binding_data(self, data: ChannelBindingData) -> None: self._channel_binding_data = data self._gs2_header = f"p={data.type},," @property def nonce_length(self) -> int: return len(self._client_nonce) @property def _b64_channel_binding_data(self) -> str: if self.name.endswith("PLUS"): assert self._channel_binding_data is not None return b64encode( b"%s%s" % (self._gs2_header.encode(), self._channel_binding_data.data) ) return b64encode(self._gs2_header) @staticmethod def _scram_parse(scram_data: str) -> dict[str, str]: return dict(s.split("=", 1) for s in scram_data.split(",")) def get_initiate_data(self) -> str: self._client_first_message_bare = "n=%s,r=%s" % ( self._username, self._client_nonce, ) client_first_message = "%s%s" % ( self._gs2_header, self._client_first_message_bare, ) return b64encode(client_first_message) def get_response_data(self, data: str | bytes) -> str: server_first_message = b64decode(data).decode() 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"]) iteration_count = int(challenge["i"]) if iteration_count < 4096: raise AuthFail("Salt iteration count to low: %s" % iteration_count) try: password = saslprep(self._password) except Exception: password = "" salted_password = pbkdf2_hmac( self._hash_method, 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) return b64encode(client_finale_message) def validate_success_data(self, data: str) -> None: server_last_message = b64decode(data).decode() success = self._scram_parse(server_last_message) server_signature = b64decode(success["v"]) if server_signature != self._server_signature: raise AuthFail("Invalid server signature") def _hmac(self, key: bytes, message: str) -> bytes: return hmac.new( key=key, msg=message.encode(), digestmod=self._hash_method ).digest() @staticmethod def _xor(x: bytes, y: bytes) -> bytes: return bytes([px ^ py for px, py in zip(x, y, strict=False)]) def _h(self, data: bytes) -> bytes: return hashlib.new(self._hash_method, data).digest() class SCRAM_SHA_1(SCRAM): name = "SCRAM-SHA-1" _hash_method = "sha1" class SCRAM_SHA_1_PLUS(SCRAM_SHA_1): name = "SCRAM-SHA-1-PLUS" class SCRAM_SHA_256(SCRAM): name = "SCRAM-SHA-256" _hash_method = "sha256" class SCRAM_SHA_256_PLUS(SCRAM_SHA_256): name = "SCRAM-SHA-256-PLUS" class SCRAM_SHA_512(SCRAM): name = "SCRAM-SHA-512" _hash_method = "sha512" class SCRAM_SHA_512_PLUS(SCRAM_SHA_512): name = "SCRAM-SHA-512-PLUS" class AuthFail(Exception): pass python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/simplexml.py000066400000000000000000000657461524361513300253660ustar00rootroot00000000000000# simplexml.py based on Mattew Allum's xmlstream.py # # Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov # # SPDX-License-Identifier: GPL-2.0-or-later """ 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 Any from typing import Union import logging import xml.parsers.expat from collections.abc import Callable from collections.abc import Iterator from copy import deepcopy from xml.parsers.expat import ExpatError 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[Node | str] parent: Node | None nsd: dict[str, str] nsp_cache: dict[Any, Any] FORCE_NODE_RECREATION = False def __init__( self, tag: str | None = None, attrs: Attrs | None = None, payload: Node | str | list[Node | str] | None = None, parent: Node | None = None, nsp: dict[Any, Any] | None = None, node_built: bool = False, node: Node | Any | None = 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: list[str] = [] self.kids = [] self.parent = node.parent self.nsd = {} for key in node.attrs: 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] = val 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 topretty( self, indent: str = "", addindent: str = " " * 2, newl: str = "\n", ) -> str: """ Write an XML element to a string string = buffer where output is added indent = current indentation addindent = indentation to add to higher levels newl = newline string """ string = indent + "<" + self.name if self.namespace: if not self.parent or self.parent.namespace != self.namespace: if "xmlns" not in self.attrs: string += f' xmlns="{self.namespace}"' for key in self.attrs: val = str(self.attrs[key]) string += f' {key}="{XMLescape(val)}"' count = 0 if self.kids: string += ">" string += newl for child in self.kids: if (len(self.data) - 1) >= count: data = XMLescape(self.data[count].strip()) if data: string += f"{indent + addindent}{XMLescape(self.data[count].strip())}{newl}" if isinstance(child, str): string += child else: string += child.topretty(indent + addindent, addindent, newl) string += newl count += 1 string += indent if (len(self.data) - 1) >= count: data = XMLescape(self.data[count].strip()) if data: string += f"{indent + addindent}{XMLescape(self.data[count].strip())}{newl}" string += f"" elif self.data: string += f">{XMLescape(self.data[0])}" else: string += " />" return string def __str__(self, fancy: int = 0) -> str: if fancy: return self.topretty() return self.topretty(indent="", addindent="", newl="") def addChild( self, name: str | None = None, attrs: Attrs | None = None, payload: list[Any] | None = None, namespace: str | None = None, node: Node | None = 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: Node | str, attrs: Attrs | None = None) -> Node | None: """ 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) 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) -> str | None: """ Return value of specified attribute """ return self.attrs.get(key) def getNamespacedAttr(self, key: str, namespace: str) -> str | None: """ Return an attribute of this Node using a different namespace than the node's main namespace. NB: This is not a full implementation. It does not search for prefixes defined in parent elements, only for prefixes defined in this `Node`. """ for prefix, ns in self.nsd.items(): if ns != namespace: continue attr = self.attrs.get(f"{prefix}:{key}") if attr is None: # there might be several prefixes defined for a given namespace continue return attr return None def getChildren(self) -> list[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) -> Node | None: """ Returns the parent of node (if present) """ return self.parent def getTag( self, name: str, attrs: Attrs | None = None, namespace: str | None = None ) -> Node | None: """ Filter all child nodes using specified arguments as filter. Return the first found or None if not found """ tags = self.getTags(name, attrs, namespace) if not tags: return None return tags[0] def getTagAttr( self, tag: str, attr: str, namespace: str | None = None ) -> str | None: """ 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) -> str | None: """ Return concatenated 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: Attrs | None = None, namespace: str | None = None ) -> list[Node]: """ Filter all child nodes using specified arguments as filter. Returns the list of nodes found """ nodes: list[Node] = [] 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: if key not in node.attrs or node.attrs[key] != attrs[key]: break else: nodes.append(node) return nodes def iterTags( self, name: str, attrs: Attrs | None = None, namespace: str | None = 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: 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: list[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: Attrs | None = None, namespace: str | None = None ) -> Node: """ Same as getTag but if the node with specified namespace/attributes not found, creates such node and returns it """ nodes = self.getTags(name, attrs, namespace=namespace) if nodes: return nodes[0] return self.addChild(name, attrs, namespace=namespace) def setTagAttr( self, tag: str, attr: str, val: str, namespace: str | None = 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: Any, attrs: Attrs | None = 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) -> str | None: 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) -> str | None: """ 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 __max_depth: int _dispatch_depth: int _document_attrs: Attrs | None _document_nsp: dict[str, str] | None _mini_dom: Node | None last_is_data: bool _ptr: Node | None data_buffer: list[str] | None stream_error: str _is_stream: bool def __init__( self, data: str | None = None, initial_node: Node | None = 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.__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.stream_error = "" 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.stream_error = children[0].getName() else: self.stream_error = 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) -> Node | None: """ 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.__depth += 1 self.__max_depth = max(self.__depth, self.__max_depth) def _dec_depth(self) -> None: self.__depth -= 1 def XML2Node(xml_str: str) -> Node | None: """ 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) -> Node | None: """ 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/smacks.py000066400000000000000000000272231524361513300246210ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import TYPE_CHECKING import logging import time from nbxmpp.const import StreamState from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Error from nbxmpp.protocol import Protocol from nbxmpp.simplexml import Node from nbxmpp.structs import StanzaHandler from nbxmpp.util import LogAdapter if TYPE_CHECKING: from nbxmpp.client import Client 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: Client) -> None: 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: list[Protocol] = [] # Unhandled stanzas queue self._old_uqueue: list[Protocol] = ( [] ) # 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: str | None = None self._location: str | None = None self._log = LogAdapter(log, {"context": client.log_context}) self.register_handlers() @property def sm_supported(self) -> bool: return self._sm_supported @sm_supported.setter def sm_supported(self, value: bool) -> None: self._log.info("Server supports detected: %s", value) self._sm_supported = value @property def resumeable(self) -> bool: return self._session_id is not None and self.resume_supported def delegate(self, stanza: Protocol) -> None: 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) -> None: 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) -> None: 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: Any, stanza: Protocol, _properties: Any) -> None: 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: str) -> None: 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: Protocol) -> None: 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: Protocol) -> None: 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) -> None: """ 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) -> None: 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: Protocol) -> None: """ 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: Any) -> None: 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) -> None: # We end the connection deliberately # Reset the state -> no resume self._log.info("Close session") self._reset_state() def request_ack(self) -> None: request = Node(Namespace.STREAM_MGMT + " r") self._log.debug("Request ack") self._client.send_nonza(request, now=False) def _on_ack(self, _stream: Any, stanza: Protocol, _properties: Any) -> None: 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: Protocol, queue: list[Protocol]) -> None: """ 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: Any, stanza: Protocol, _properties: Any) -> None: """ 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) -> None: # 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/stringprep.py000066400000000000000000000160531524361513300255340ustar00rootroot00000000000000# This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later # Initial version taken from https://github.com/horazont/aioxmpp # Modified on 30-AUG-2022 from __future__ import annotations import stringprep from collections.abc import Callable from unicodedata import ucd_3_2_0 _nodeprep_prohibited = frozenset("\"&'/:<>@") def is_RandALCat(c: str) -> bool: return ucd_3_2_0.bidirectional(c) in ("R", "AL") def is_LCat(c: str) -> bool: return ucd_3_2_0.bidirectional(c) == "L" def check_nodeprep_prohibited(char: str) -> bool: return char in _nodeprep_prohibited def _check_against_tables( chars: list[str], tables: tuple[Callable[[str], bool], ...] ) -> str | None: """ Perform a check against the table predicates in `tables`. `tables` must be a reusable iterable containing characteristic functions of character sets, that is, functions which return :data:`True` if the character is in the table. The function returns the first character occuring in any of the tables or :data:`None` if no character matches. """ for c in chars: if any(in_table(c) for in_table in tables): return c return None def do_normalization(chars: list[str]) -> None: """ Perform the stringprep normalization. Operates in-place on a list of unicode characters provided in `chars`. """ chars[:] = list(ucd_3_2_0.normalize("NFKC", "".join(chars))) def check_bidi(chars: list[str]) -> None: """ Check proper bidirectionality as per stringprep. Operates on a list of unicode characters provided in `chars`. """ # the empty string is valid, as it cannot violate the RandALCat constraints if not chars: return # first_is_RorAL = ucd_3_2_0.bidirectional(chars[0]) in {"R", "AL"} # if first_is_RorAL: has_RandALCat = any(is_RandALCat(c) for c in chars) if not has_RandALCat: return has_LCat = any(is_LCat(c) for c in chars) if has_LCat: raise ValueError("L and R/AL characters must not occur in the same" " string") if not is_RandALCat(chars[0]) or not is_RandALCat(chars[-1]): raise ValueError("R/AL string must start and end with R/AL character.") def check_against_tables( chars: list[str], bad_tables: tuple[Callable[[str], bool], ...] ) -> None: """ Check against tables, by checking whether any of the characters from `chars` are in any of the `bad_tables`. Operates in-place on a list of code points from `chars`. """ violator = _check_against_tables(chars, bad_tables) if violator is not None: raise ValueError( "Input contains prohibited or unassigned codepoint: " "U+{:04x}".format(ord(violator)) ) def _nodeprep_do_mapping(chars: list[str]) -> None: i = 0 while i < len(chars): c = chars[i] if stringprep.in_table_b1(c): del chars[i] else: replacement = stringprep.map_table_b2(c) if replacement != c: chars[i : (i + 1)] = list(replacement) i += len(replacement) def nodeprep(string: str, allow_unassigned: bool = False) -> str: """ Process the given `string` using the Nodeprep (`RFC 6122`_) profile. In the error cases defined in `RFC 3454`_ (stringprep), a :class:`ValueError` is raised. """ chars = list(string) _nodeprep_do_mapping(chars) do_normalization(chars) check_against_tables( chars, ( stringprep.in_table_c11, stringprep.in_table_c12, stringprep.in_table_c21, stringprep.in_table_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9, check_nodeprep_prohibited, ), ) check_bidi(chars) if not allow_unassigned: check_against_tables(chars, (stringprep.in_table_a1,)) return "".join(chars) def _resourceprep_do_mapping(chars: list[str]) -> None: i = 0 while i < len(chars): c = chars[i] if stringprep.in_table_b1(c): del chars[i] continue i += 1 def resourceprep(string: str, allow_unassigned: bool = False) -> str: """ Process the given `string` using the Resourceprep (`RFC 6122`_) profile. In the error cases defined in `RFC 3454`_ (stringprep), a :class:`ValueError` is raised. """ chars = list(string) _resourceprep_do_mapping(chars) do_normalization(chars) check_against_tables( chars, ( stringprep.in_table_c12, stringprep.in_table_c21, stringprep.in_table_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9, ), ) check_bidi(chars) if not allow_unassigned: check_against_tables(chars, (stringprep.in_table_a1,)) return "".join(chars) def nameprep(string: str, allow_unassigned: bool = False) -> str: """ Process the given `string` using the Nameprep (`RFC 3491`_) profile. In the error cases defined in `RFC 3454`_ (stringprep), a :class:`ValueError` is raised. """ chars = list(string) _nodeprep_do_mapping(chars) do_normalization(chars) check_against_tables( chars, ( stringprep.in_table_c12, stringprep.in_table_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9, ), ) check_bidi(chars) if not allow_unassigned: check_against_tables(chars, (stringprep.in_table_a1,)) return "".join(chars) def _saslprep_do_mapping(chars: list[str]) -> None: i = 0 while i < len(chars): c = chars[i] if stringprep.in_table_b1(c): del chars[i] elif stringprep.in_table_c12(c): chars[i] = " " i += 1 def saslprep(string: str, allow_unassigned: bool = False) -> str: """ Process the given `string` using the SASLprep (`RFC 4013`_) profile. """ chars = list(string) _saslprep_do_mapping(chars) do_normalization(chars) check_against_tables( chars, ( stringprep.in_table_c12, stringprep.in_table_c21, stringprep.in_table_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9, ), ) check_bidi(chars) if not allow_unassigned: check_against_tables(chars, (stringprep.in_table_a1,)) return "".join(chars) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/structs.py000066400000000000000000001150151524361513300250440ustar00rootroot00000000000000# Copyright (C) 2018-2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations import typing from typing import Any from typing import NamedTuple from typing import TYPE_CHECKING import logging import secrets import time from collections.abc import Sequence from dataclasses import dataclass from dataclasses import field from datetime import datetime from gi.repository import Gio from gi.repository import GLib from nbxmpp import exceptions from nbxmpp.const import AdHocAction from nbxmpp.const import AdHocNoteType from nbxmpp.const import AdHocStatus from nbxmpp.const import Affiliation from nbxmpp.const import AnonymityMode from nbxmpp.const import AvatarState from nbxmpp.const import Chatstate from nbxmpp.const import ConnectionProtocol from nbxmpp.const import ConnectionType from nbxmpp.const import InviteType from nbxmpp.const import IqType from nbxmpp.const import MessageType from nbxmpp.const import PresenceShow from nbxmpp.const import PresenceType from nbxmpp.const import Role from nbxmpp.const import StatusCode from nbxmpp.language import LanguageMap from nbxmpp.language import LanguageRange from nbxmpp.language import LanguageTag from nbxmpp.modules.dataforms import MultipleDataForm from nbxmpp.modules.dataforms import SimpleDataForm from nbxmpp.modules.fallback import FallbacksForT from nbxmpp.modules.fallback import strip_fallback from nbxmpp.namespaces import Namespace from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.protocol import Protocol from nbxmpp.simplexml import Node if TYPE_CHECKING: from nbxmpp.modules.security_labels import SecurityLabel log = logging.getLogger("nbxmpp.structs") class StanzaHandler(NamedTuple): name: str callback: Any typ: str = "" ns: str = "" xmlns: str | None = None priority: int = 50 class CommonResult(NamedTuple): jid: JID | None = None class InviteData(NamedTuple): muc: JID from_: JID reason: str | None password: str | None type: InviteType continued: bool thread: str | None class DeclineData(NamedTuple): muc: JID from_: JID reason: str | None class CaptchaData(NamedTuple): form: Any bob_data: BobData class BobData(NamedTuple): algo: str hash_: str max_age: int data: bytes cid: str type: str class BookmarkData(NamedTuple): jid: JID name: str | None = None nick: str | None = None autojoin: bool = False password: str | None = None extensions: Node | None = None class VoiceRequest(NamedTuple): jid: JID nick: str form: Any class MucUserData(NamedTuple): jid: JID | None affiliation: Affiliation | None nick: str | None role: Role | None actor: str | None reason: str | None class MucDestroyed(NamedTuple): alternate: JID | None reason: str | None password: str | None class MucConfigResult(NamedTuple): jid: JID form: Any | None = None class MucSubject(NamedTuple): text: str author: str | None timestamp: float | None class AffiliationResult(NamedTuple): jid: JID users: dict[JID, dict[str, str]] class EntityCapsData(NamedTuple): hash: str node: str ver: str class HTTPAuthData(NamedTuple): id: str | None method: str | None url: str | None body: str | None class StanzaIDData(NamedTuple): id: str by: str class PubSubEventData(NamedTuple): node: str id: str | None = None item: Node | None = None data: Any | None = None deleted: bool = False retracted: bool = False purged: bool = False class MoodData(NamedTuple): mood: str test: str class BlockingPush(NamedTuple): block: set[JID] unblock: set[JID] unblock_all: bool class ActivityData(NamedTuple): activity: str subactivity: str text: str class LocationData(NamedTuple): accuracy: str | None = None alt: str | None = None altaccuracy: str | None = None area: str | None = None bearing: str | None = None building: str | None = None country: str | None = None countrycode: str | None = None datum: str | None = None description: str | None = None error: str | None = None floor: str | None = None lat: str | None = None locality: str | None = None lon: str | None = None postalcode: str | None = None region: str | None = None room: str | None = None speed: str | None = None street: str | None = None text: str | None = None timestamp: str | None = None tzo: str | None = None uri: str | None = None class PGPPublicKey(NamedTuple): jid: JID key: bytes date: float class PGPKeyMetadata(NamedTuple): jid: JID fingerprint: str date: float class OMEMOMessage(NamedTuple): sid: int iv: bytes keys: dict[int, tuple[bytes, bool]] payload: bytes | None class AnnotationNote(NamedTuple): jid: JID data: str cdate: datetime | None = None mdate: datetime | None = None class EMEData(NamedTuple): name: str namespace: str class MuclumbusResult(NamedTuple): first: str | None last: str | None max: int | None end: bool items: list[MuclumbusItem] class MuclumbusItem(NamedTuple): jid: str name: str nusers: str description: str language: str is_open: bool anonymity_mode: AnonymityMode class SoftwareVersionResult(NamedTuple): name: str version: str os: str | None class AdHocCommandNote(NamedTuple): text: str type: AdHocNoteType class IBBData(NamedTuple): type: str sid: str block_size: int | None = None seq: int | None = None data: bytes | None = None class OOBData(NamedTuple): url: str desc: str class CorrectionData(NamedTuple): id: str class ReplyData(NamedTuple): to: JID id: str class ModerationData(NamedTuple): stanza_id: str by: JID | None reason: str | None stamp: datetime is_tombstone: bool occupant_id: str | None class RetractionData(NamedTuple): id: str is_tombstone: bool timestamp: datetime class DiscoItems(NamedTuple): jid: JID node: str items: list[DiscoItem] class DiscoItem(NamedTuple): jid: JID name: str | None node: str | None class RegisterData(NamedTuple): instructions: str | None form: Any | None fields_form: Any | None oob_url: str | None bob_data: BobData | None class HTTPUploadData(NamedTuple): put_uri: str get_uri: str headers: dict[str, str] class RSMData(NamedTuple): after: str | None before: str | None last: str | None first: str | None first_index: int | None count: int | None max: int | None index: int | None class MAMQueryData(NamedTuple): jid: JID rsm: RSMData complete: bool class MAMPreferencesData(NamedTuple): default: str always: list[JID] never: list[JID] class LastActivityData(NamedTuple): seconds: int status: str class RosterData(NamedTuple): items: list[RosterItem] | None version: str | None class RosterPush(NamedTuple): item: RosterItem version: str class ServerAddress(NamedTuple): domain: str | None service: str | None host: str | None uri: str | None protocol: ConnectionProtocol type: ConnectionType proxy: ProxyData | None @property def is_service(self) -> bool: return self.service is not None @property def is_host(self) -> bool: return self.host is not None @property def is_uri(self) -> bool: return self.uri is not None def has_proxy(self) -> bool: return self.proxy is not None @dataclass class RosterItem: jid: JID name: str | None = None ask: str | None = None subscription: str | None = None approved: str | None = None groups: set[str] = field(default_factory=set) @classmethod def from_node(cls, node: Node) -> RosterItem: attrs = node.getAttrs(copy=True) jid = attrs.get("jid") if jid is None: raise Exception("jid attribute missing") jid = JID.from_string(jid) if jid.is_full: raise Exception("full jid in roster not allowed") groups = {group.getData() for group in node.getTags("group")} return cls( jid=jid, name=attrs.get("name"), ask=attrs.get("ask"), subscription=attrs.get("subscription") or "none", approved=attrs.get("approved"), groups=groups, ) def asdict(self) -> dict[str, Any]: return { "jid": self.jid, "name": self.name, "ask": self.ask, "subscription": self.subscription, "approved": self.approved, "groups": self.groups, } class DiscoInfo(NamedTuple): stanza: Iq | None identities: list[DiscoIdentity] features: list[str] dataforms: list[SimpleDataForm | MultipleDataForm] timestamp: float | None = None def get_caps_hash(self) -> str | None: try: return self.node.split("#")[1] except Exception: return None def has_field(self, form_type: str, var: str) -> bool: 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: str, var: str) -> Any | None: for dataform in self.dataforms: try: if dataform["FORM_TYPE"].value != form_type: continue if dataform[var].type_ == "jid-multi" or dataform[var].type_.startswith( "list-" ): return dataform[var].values or None return dataform[var].value except Exception: continue return None def supports(self, feature: str) -> bool: return feature in self.features def serialize(self) -> str: if self.stanza is None: raise ValueError("Unable to serialize DiscoInfo, no stanza found") return str(self.stanza) @property def node(self) -> str | None: try: query = self.stanza.getQuery() except Exception: return None if query is not None: return query.getAttr("node") return None @property def jid(self) -> JID | None: try: return self.stanza.getFrom() except Exception: return None @property def mam_namespace(self) -> str | None: 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) -> bool: return Namespace.MAM_2 in self.features @property def has_mam_1(self) -> bool: return Namespace.MAM_1 in self.features @property def has_mam(self) -> bool: return self.has_mam_1 or self.has_mam_2 @property def has_httpupload(self) -> bool: return Namespace.HTTPUPLOAD_0 in self.features @property def has_message_moderation(self) -> bool: return ( Namespace.MESSAGE_MODERATE in self.features or Namespace.MESSAGE_MODERATE_1 in self.features ) @property def moderation_namespace(self) -> str | None: if Namespace.MESSAGE_MODERATE_1 in self.features: return Namespace.MESSAGE_MODERATE_1 if Namespace.MESSAGE_MODERATE in self.features: return Namespace.MESSAGE_MODERATE return None @property def is_muc(self) -> bool: for identity in self.identities: if identity.category == "conference": if Namespace.MUC in self.features: return True return False @property def is_irc(self) -> bool: for identity in self.identities: if identity.category == "conference" and identity.type == "irc": return True return False @property def muc_name(self) -> str | None: 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) -> str | None: for identity in self.identities: if identity.category == "conference": return identity.name return None @property def muc_room_name(self) -> Any | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roomconfig_roomname") @property def muc_description(self) -> Any | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_description") @property def muc_log_uri(self) -> Any | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_logs") @property def muc_users(self) -> Any | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_occupants") @property def muc_contacts(self) -> Any | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_contactjid") @property def muc_subject(self) -> Any | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_subject") @property def muc_subjectmod(self) -> Any | None: # muc#roominfo_changesubject stems from a wrong example in the MUC XEP # Ejabberd and Prosody use this value # muc#roomconfig_changesubject is also used by Prosody return ( self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_subjectmod") or self.get_field_value(Namespace.MUC_INFO, "muc#roomconfig_changesubject") or self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_changesubject") ) @property def muc_lang(self) -> Any | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roominfo_lang") @property def muc_allows_invites(self) -> bool | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roomconfig_allowinvites") @property def prosody_allow_member_invites(self) -> bool | None: return self.get_field_value( Namespace.MUC_INFO, r"{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", ) @property def roomconfig_allowpm(self) -> str | None: return self.get_field_value(Namespace.MUC_INFO, "muc#roomconfig_allowpm") @property def muc_is_persistent(self) -> bool: return "muc_persistent" in self.features @property def muc_is_moderated(self) -> bool: return "muc_moderated" in self.features @property def muc_is_open(self) -> bool: return "muc_open" in self.features @property def muc_is_members_only(self) -> bool: return "muc_membersonly" in self.features @property def muc_is_hidden(self) -> bool: return "muc_hidden" in self.features @property def muc_is_nonanonymous(self) -> bool: return "muc_nonanonymous" in self.features @property def muc_is_passwordprotected(self) -> bool: return "muc_passwordprotected" in self.features @property def muc_is_public(self) -> bool: return "muc_public" in self.features @property def muc_is_semianonymous(self) -> bool: return "muc_semianonymous" in self.features @property def muc_is_temporary(self) -> bool: return "muc_temporary" in self.features @property def muc_is_unmoderated(self) -> bool: return "muc_unmoderated" in self.features @property def muc_is_unsecured(self) -> bool: return "muc_unsecured" in self.features @property def is_gateway(self) -> bool: return any(identity.category == "gateway" for identity in self.identities) @property def gateway_name(self) -> str | None: for identity in self.identities: if identity.category == "gateway": return identity.name return None @property def gateway_type(self) -> str | None: for identity in self.identities: if identity.category == "gateway": return identity.type return None def has_category(self, category: str) -> bool: return any(identity.category == category for identity in self.identities) def has_identity(self, category: str, type_: str) -> bool: for identity in self.identities: if identity.category == category and identity.type == type_: return True return False @property def httpupload_max_file_size(self) -> float | None: size = self.get_field_value(Namespace.HTTPUPLOAD_0, "max-file-size") try: return float(size) except Exception: return None @property def reactions_per_user(self) -> int | None: value = self.get_field_value( Namespace.REACTIONS_RESTRICTIONS, "max_reactions_per_user" ) if value is None: return None try: return int(value) except ValueError: return None @property def reactions_allowlist(self) -> list[str] | None: value = self.get_field_value(Namespace.REACTIONS_RESTRICTIONS, "allowlist") return value class DiscoIdentity(NamedTuple): category: str type: str name: str | None = None lang: str | None = None def get_node(self) -> Node: 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: object) -> bool: return str(self) == str(other) def __ne__(self, other: object) -> bool: return not self.__eq__(other) def __str__(self) -> str: return "%s/%s/%s/%s" % ( self.category, self.type, self.lang or "", self.name or "", ) def __hash__(self) -> int: return hash(str(self)) class AdHocCommand(NamedTuple): jid: JID node: str name: str | None sessionid: str | None = None status: AdHocStatus | None = None data: Node | None = None actions: set[AdHocAction] | None = None default: AdHocAction | None = None notes: list[AdHocCommandNote] | None = None @property def is_completed(self) -> bool: return self.status == AdHocStatus.COMPLETED @property def is_canceled(self) -> bool: return self.status == AdHocStatus.CANCELED class ProxyData(NamedTuple): type: str host: str username: str | None password: str | None def get_uri(self) -> str: if self.username is not None: username = GLib.uri_escape_string(self.username, None, False) password = GLib.uri_escape_string(self.password, None, False) user_pass = f"{username}:{password}" return "%s://%s@%s" % (self.type, user_pass, self.host) return "%s://%s" % (self.type, self.host) def get_resolver(self) -> Gio.SimpleProxyResolver: return Gio.SimpleProxyResolver.new(self.get_uri(), None) class OMEMOBundle(NamedTuple): spk: dict[str, int | bytes] spk_signature: bytes ik: bytes otpks: list[dict[str, str]] device_id: int = -1 namespace: str = Namespace.OMEMO_TEMP def pick_prekey(self) -> dict[str, str]: return secrets.choice(self.otpks) class ChatMarker(NamedTuple): type: str id: str @property def is_received(self) -> bool: return self.type == "received" @property def is_displayed(self) -> bool: return self.type == "displayed" @property def is_acknowledged(self) -> bool: return self.type == "acknowledged" class Reactions(NamedTuple): id: str emojis: set[str] class CommonError: def __init__(self, stanza: Protocol) -> None: 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.by = None self.jid = stanza.getFrom() self.id = stanza.getID() self._text: dict[str, str] = {} by = self._error_node.getAttr("by") if by is not None: try: self.by = JID.from_string(by) except Exception: pass 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: bytes | str) -> CommonError: return cls(Protocol(node=node_string)) def get_text(self, pref_lang: str | None = None) -> str: 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 next(iter(self._text.values())) return "" def set_text(self, lang: str, text: str) -> None: self._text[lang] = text def __str__(self) -> str: 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) -> str: 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: Protocol) -> None: CommonError.__init__(self, stanza) def get_max_file_size(self) -> float | None: 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 StreamError(CommonError): def __init__(self, stanza: Protocol) -> None: 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: dict[str, str] = {} 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: str) -> Any: raise NotImplementedError def __str__(self) -> str: 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) -> str: raise NotImplementedError class TuneData(NamedTuple): artist: str | None = None length: str | None = None rating: str | None = None source: str | None = None title: str | None = None track: str | None = None uri: str | None = None @property def was_removed(self) -> bool: return self.artist is None and self.title is None and self.track is None class MAMData(NamedTuple): id: str query_id: str archive: JID namespace: str timestamp: float @property def is_ver_1(self) -> bool: return self.namespace == Namespace.MAM_1 @property def is_ver_2(self) -> bool: return self.namespace == Namespace.MAM_2 class CarbonData(NamedTuple): type: str @property def is_sent(self) -> bool: return self.type == "sent" @property def is_received(self) -> bool: return self.type == "received" class ReceiptData(NamedTuple): type: str id: str | None = None @property def is_request(self) -> bool: return self.type == "request" @property def is_received(self) -> bool: return self.type == "received" @dataclass class Hat: uri: str title: str hue: float | None = None class HatData: def __init__(self) -> None: self._hat_map = LanguageMap() def add_hat(self, hat: Hat, lang: str | None) -> None: language_tag = LanguageTag(tag=lang) if lang else None if language_tag not in self._hat_map: self._hat_map[language_tag] = [] self._hat_map[language_tag].append(hat) def get_hats( self, language_range: Sequence[LanguageRange] | None = None ) -> list[Hat]: if language_range is None: return self._hat_map.any() try: return self._hat_map.lookup(language_range) except KeyError: return self._hat_map.any() def __eq__(self, other: object) -> bool: if not isinstance(other, HatData): return False return self._hat_map == other._hat_map def __ne__(self, other: object) -> bool: return not self.__eq__(other) @dataclass class EncryptionData: protocol: str key: str | None trust: int @dataclass class OpenGraphData: title: str | None = None description: str | None = None url: str | None = None image: str | None = None type: str | None = None site_name: str | None = None class Properties: pass @dataclass class MessageProperties: own_jid: JID carbon: CarbonData | None = None type: MessageType = MessageType.NORMAL id: str | None = None stanza_ids: list[StanzaIDData] = field(default_factory=list) origin_id: str | None = None from_: JID | None = None to: JID | None = None jid: JID | None = None remote_jid: JID | None = None subject: str | None = None body: str | None = None bodies: BodyData | None = None thread: str | None = None user_timestamp: float | None = None timestamp: float = field(default_factory=time.time) has_server_delay: bool = False error = None eme: EMEData | None = None http_auth: HTTPAuthData | None = None nickname: str | None = None from_muc: bool = False occupant_id: str | None = None muc_jid: JID | None = None muc_nickname: str | None = None muc_status_codes: set[StatusCode] | None = None muc_private_message: bool = False muc_invite: InviteData | None = None muc_decline: DeclineData | None = None muc_user: MucUserData | None = None muc_ofrom: JID | None = None muc_subject: MucSubject | None = None captcha: CaptchaData | None = None voice_request: VoiceRequest | None = None self_message: bool = False mam: MAMData | None = None pubsub: bool = False pubsub_event: PubSubEventData | None = None openpgp: bytes | None = None omemo: OMEMOMessage | None = None encrypted: EncryptionData | None = None pgp_legacy: str | None = None marker: ChatMarker | None = None receipt: ReceiptData | None = None oob: OOBData | None = None correction: CorrectionData | None = None reply_data: ReplyData | None = None moderation: ModerationData | None = None retraction: RetractionData | None = None attention: bool = False forms = None xhtml: XHTMLData | None = None security_label: SecurityLabel | None = None chatstate: Chatstate | None = None reactions: Reactions | None = None # keys of the open_graph dict are URLs defined by the "about" attr of the # corresponding element (RDF namespace) open_graph: dict[str, OpenGraphData] = field(default_factory=dict) def is_from_us(self, bare_match: bool = True) -> bool: 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) -> bool: return self.user_timestamp is not None @property def is_encrypted(self) -> bool: return self.encrypted is not None @property def is_omemo(self) -> bool: return self.omemo is not None @property def is_openpgp(self) -> bool: return self.openpgp is not None @property def is_pgp_legacy(self) -> bool: return self.pgp_legacy is not None @property def is_pubsub(self) -> bool: return self.pubsub @property def is_pubsub_event(self) -> bool: return self.pubsub_event is not None @property def is_carbon_message(self) -> bool: return self.carbon is not None @property def is_sent_carbon(self) -> bool: return self.carbon is not None and self.carbon.is_sent @property def is_received_carbon(self) -> bool: return self.carbon is not None and self.carbon.is_received @property def is_mam_message(self) -> bool: return self.mam is not None @property def is_http_auth(self) -> bool: return self.http_auth is not None @property def is_muc_subject(self) -> bool: return ( self.type == MessageType.GROUPCHAT and self.body is None and self.thread is None and self.subject is not None ) @property def is_muc_config_change(self) -> bool: return bool(self.muc_status_codes) @property def is_muc_pm(self) -> bool: return self.muc_private_message @property def is_muc_invite_or_decline(self) -> bool: return self.muc_invite is not None or self.muc_decline is not None @property def is_captcha_challenge(self) -> bool: return self.captcha is not None @property def is_voice_request(self) -> bool: return self.voice_request is not None @property def is_self_message(self) -> bool: return self.self_message @property def is_marker(self) -> bool: return self.marker is not None @property def is_receipt(self) -> bool: return self.receipt is not None @property def is_oob(self) -> bool: return self.oob is not None @property def is_correction(self) -> bool: return self.correction is not None @property def is_moderation(self) -> bool: return self.moderation is not None @property def is_retraction(self) -> bool: return self.retraction is not None @property def has_attention(self) -> bool: return self.attention @property def has_forms(self) -> bool: return self.forms is not None @property def has_xhtml(self) -> bool: return self.xhtml is not None @property def has_security_label(self) -> bool: return self.security_label is not None @property def has_chatstate(self) -> bool: return self.chatstate is not None @dataclass class IqProperties: own_jid: JID type: IqType | None = None jid: JID | None = None id: str | None = None error: Any | None = None query: Node | None = None payload: Node | None = None http_auth: HTTPAuthData | None = None ibb: IBBData | None = None blocking: BlockingPush | None = None roster: RosterPush | None = None @property def is_http_auth(self) -> bool: return self.http_auth is not None @property def is_ibb(self) -> bool: return self.ibb is not None @property def is_blocking(self) -> bool: return self.blocking is not None @property def is_roster(self) -> bool: return self.roster is not None class IqPropertiesBase(typing.Protocol): own_jid: JID type: IqType jid: JID id: str error: Any | None query: Node | None payload: Node | None class BlockingProperties(IqPropertiesBase): blocking: BlockingPush @property def is_blocking(self) -> bool: ... @dataclass class PresenceProperties: own_jid: JID type: PresenceType | None = None priority: int | None = None show: PresenceShow | None = None jid: JID | None = None resource: str | None = None id: str | None = None nickname: str | None = None self_presence: bool = False self_bare: bool = False from_muc: bool = False occupant_id: str | None = None status: str = "" timestamp: float = field(default_factory=time.time) user_timestamp: float | None = None idle_timestamp: float | None = None signed: Any | None = None error: Any | None = None avatar_sha: str | None = None avatar_state: AvatarState = AvatarState.IGNORE muc_jid: JID | None = None muc_status_codes: set[StatusCode] | None = None muc_user: MucUserData | None = None muc_nickname: str | None = None muc_destroyed: MucDestroyed | None = None entity_caps: EntityCapsData | None = None hats: HatData | None = None @property def is_self_presence(self) -> bool: return self.self_presence @property def is_self_bare(self) -> bool: return self.self_bare @property def is_muc_destroyed(self) -> bool: return self.muc_destroyed is not None @property def is_muc_self_presence(self) -> bool: 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) -> bool: 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) -> bool: 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) -> JID: 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) -> bool: 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 bool(status_codes.intersection(self.muc_status_codes)) and self.type == PresenceType.UNAVAILABLE ) @property def is_muc_shutdown(self) -> bool: 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) -> bool: 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) -> Affiliation | None: try: return self.muc_user.affiliation except Exception: return None @property def role(self) -> Role | None: try: return self.muc_user.role except Exception: return None class XHTMLData: def __init__(self, xhtml: Node) -> None: self._bodys: dict[str | None, Node] = {} for body in xhtml.getTags("body", namespace=Namespace.XHTML): lang = body.getXmlLang() self._bodys[lang] = body def get_body(self, pref_lang: str | None = None) -> str: 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]) @dataclass class ChannelBindingData: type: Gio.TlsChannelBindingType data: bytes @dataclass class MDSData: jid: JID stanza_id: str stanza_id_by: JID @dataclass class BodyData: def __init__( self, stanza: Node, fallbacks_for: FallbacksForT | None = None, fallback_ns: set[str] | None = None, ) -> None: self._body_map = LanguageMap() self._fallbacks_for = fallbacks_for self._fallback_ns = fallback_ns for body in stanza.getTags("body"): lang = body.getAttr("xml:lang") lang_tag = LanguageTag(tag=lang) if lang else None self._body_map[lang_tag] = (lang, body.getData()) def get( self, language_range: Sequence[LanguageRange] | None, ) -> str: if not self._body_map: return "" if language_range is None: lang, text = self._body_map.any() else: try: lang, text = self._body_map.lookup(language_range) except KeyError: lang, text = self._body_map.any() if not self._fallbacks_for or not self._fallback_ns: return text try: return strip_fallback(self._fallbacks_for, self._fallback_ns, lang, text) except exceptions.FallbackLanguageError: log.warning("Missing fallback for language: %s", lang) return text @dataclass class AccountInviteResult: uri: str landing_url: str | None expire: datetime | None python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/task.py000066400000000000000000000247551524361513300243110ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import ParamSpec from typing import TYPE_CHECKING from typing import TypeVar import inspect import logging import weakref from collections.abc import Callable from collections.abc import Generator from enum import IntEnum from functools import wraps from gi.repository import GLib from nbxmpp.errors import BaseError from nbxmpp.errors import CancelledError from nbxmpp.errors import is_error from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import StanzaError from nbxmpp.errors import TimeoutStanzaError from nbxmpp.modules.util import make_func_arguments_string from nbxmpp.simplexml import Node if TYPE_CHECKING: from nbxmpp.client import Client from nbxmpp.dispatcher import NBXMPPModuleT log = logging.getLogger("nbxmpp.task") class _ResultSet: pass ResultSet = _ResultSet() T = TypeVar("T") P = ParamSpec("P") 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: Task, client: Client, callback: Callable[..., Any] | None, user_data: Any ) -> Task: 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: Callable[..., T]) -> Callable[..., T]: @wraps(func) def func_wrapper( self: NBXMPPModuleT, *args: Any, timeout: int | None = None, callback: Callable[..., Any] | None = None, user_data: Any = None, **kwargs: Any, ) -> T: if self._log.isEnabledFor(logging.INFO): # type: ignore self._log.info(make_func_arguments_string(func, self, args, kwargs)) # type: ignore task = IqRequestTask( func(self, *args, **kwargs), self._log, self._client # type: ignore ) task.set_timeout(timeout) return _setup_task(task, self._client, callback, user_data) # type: ignore return func_wrapper def is_fatal_error(error: BaseError | Exception | Any) -> bool: if is_error(error): assert isinstance(error, BaseError) 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: Generator[Any, Any, Any], logger: logging.Logger = log ) -> None: self._logger = logger self._gen = gen self._done_callbacks: list[Callable[..., Any]] = [] self._sub_task: Task | None = None self._result = None self._error = None self._user_data: Any | None = None self._timeout: int | None = None self._timeout_id: int | None = None self._finalize_func: Callable[..., Any] | None = None self._finalize_context: Any = None self._state = TaskState.INIT @property def state(self) -> TaskState: return self._state def add_done_callback( self, callback: Callable[..., Any], weak: bool = True ) -> None: 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: raise ValueError("Unknown callback object") self._done_callbacks.append(callback) def set_timeout(self, timeout: int | None) -> None: self._timeout = timeout def start(self) -> None: if not self._state.is_init: raise RuntimeError("Task already started") if self._timeout is not None: self._logger.info( "Add timeout for task: %s s, task id: %s", self._timeout, id(self) ) self._timeout_id = GLib.timeout_add_seconds(self._timeout, self._on_timeout) self._state = TaskState.RUNNING next(self._gen) self._next_step(self) def _run_async(self, data: Any) -> None: raise NotImplementedError def _async_finished(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError def _sub_task_completed(self, task: Task) -> None: 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: Any) -> None: 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: raise 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) -> None: self._state = TaskState.FINISHED self._invoke_callbacks() self._finalize() def _log_if_fatal(self, error: BaseError | Exception | Any) -> None: if is_error(error): assert isinstance( error, StanzaError | MalformedStanzaError | TimeoutStanzaError | CancelledError, ) if error.is_fatal: self._logger.log(error.log_level, error) elif isinstance(error, Exception): self._logger.exception("Fatal Exception") def _invoke_callbacks(self) -> None: for callback in self._done_callbacks: if isinstance(callback, weakref.WeakMethod | weakref.ref): callback = callback() # type: ignore 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: Any): self._result = result return ResultSet def get_result(self) -> Any: # 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) -> Any: if self._error is not None: raise self._error return self._result def set_user_data(self, user_data: Any) -> None: self._user_data = user_data def get_user_data(self) -> Any: return self._user_data def set_finalize_func(self, func: Callable[..., Any], context: Any = None) -> None: self._finalize_func = func self._finalize_context = context def _on_timeout(self) -> None: self._logger.info("Timeout reached, task id: %s", id(self)) if not self._state.is_running: return self._timeout_id = None if self._sub_task is not None: self._sub_task.cancel(invoke_callbacks=False) self._error = TimeoutStanzaError() self._set_finished() def cancel(self, invoke_callbacks: bool = True) -> None: if not self._state.is_running: return self._state = TaskState.CANCELLED if self._sub_task is not None: self._sub_task.cancel(invoke_callbacks=False) self._error = CancelledError() if invoke_callbacks: self._invoke_callbacks() self._finalize() def _finalize(self) -> None: if self._timeout_id is not None: GLib.source_remove(self._timeout_id) self._timeout_id = None 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: Generator[Any, Any, Any], logger: logging.Logger, client: Client ) -> None: super().__init__(gen, logger) self._client = client self._iq_id: str | None = None def _run_async(self, data: Node) -> None: assert self._client is not None self._iq_id = self._client.send_stanza( data, callback=self._async_finished, timeout=self._timeout ) def _async_finished( self, _client: Client, result: Any, *args: Any, **kwargs: Any ) -> None: if self._state == TaskState.CANCELLED: return if result is None: self._error = TimeoutStanzaError() self._set_finished() return self._next_step(result) def _finalize(self) -> None: if self._iq_id is not None: self._client._dispatcher.remove_iq_callback(self._iq_id) # type: ignore self._client = None super()._finalize() python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/tcp.py000066400000000000000000000313311524361513300241210ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from typing import Any import logging from collections import deque from gi.repository import Gio from gi.repository import GLib from gi.repository import GObject from nbxmpp.connection import Connection from nbxmpp.const import ConnectionType from nbxmpp.const import TCPState from nbxmpp.protocol import Protocol from nbxmpp.util import convert_tls_error_flags from nbxmpp.util import utf8_decode log = logging.getLogger("nbxmpp.tcp") READ_BUFFER_SIZE = 8192 class TCPConnection(Connection): def __init__(self, *args: Any, **kwargs: Any) -> None: Connection.__init__(self, *args, **kwargs) self._client = Gio.SocketClient.new() self._client.set_protocol(Gio.SocketProtocol.TCP) 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: Gio.SocketConnection | None = None self._read_buffer = b"" self._write_queue: deque[Protocol] | None = deque([]) self._write_stanza_buffer: deque[Protocol] | None = 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 def connect(self) -> None: 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: Gio.SocketClient, event: Gio.SocketClientEvent, _connectable: Gio.SocketConnectable, connection: Gio.SocketConnection, ) -> None: 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: Gio.TlsClientConnection, certificate: Gio.TlsCertificate, errors: Gio.TlsCertificateFlags, ) -> bool: 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: Gio.TlsClientConnection, _param: Any ) -> None: if self._peer_certificate is None: # If the cert has errors _check_certificate() will set the cert and # _accept_certificate() will modify the error set. If this is the # case _accept_certificate() modifies the errors. 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 def _on_connect_finished( self, client: Gio.SocketClient, result: Gio.AsyncResult, _user_data: Any ) -> None: 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) -> None: self.notify("connected") self._read_async() def start_tls_negotiation(self) -> None: 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 ) self._tls_con = Gio.TlsClientConnection.new(self._con, identity) if self._address.type == ConnectionType.DIRECT_TLS: self._tls_con.set_advertised_protocols(["xmpp-client"]) self._tls_con.connect("accept-certificate", self._check_certificate) self._tls_con.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(self._tls_con, self._con.get_socket()) def _read_async(self) -> None: if self._input_closed: return self._con.get_input_stream().read_bytes_async( READ_BUFFER_SIZE, GLib.PRIORITY_LOW, self._read_cancellable, self._on_read_async_finish, None, ) def _on_read_async_finish( self, stream: Gio.InputStream, result: Gio.AsyncResult, _user_data: Any ) -> None: 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: self._log.info("Reveived zero data on _read_async()") self._finalize("disconnected") return 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) -> None: 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: bytes) -> None: # 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: Gio.OutputStream, result: Gio.AsyncResult, data: bytes ) -> None: 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 decoded_data = data.decode() self._log_stanza(decoded_data, received=False) 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: Protocol, now: bool = False) -> None: 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) -> None: 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) -> None: if self._input_closed and self._output_closed: self._finalize("disconnected") def shutdown_input(self) -> None: self._log.info("Shutdown input") self._input_closed = True self._read_cancellable.cancel() self._check_for_shutdown() def shutdown_output(self) -> None: self.state = TCPState.DISCONNECTING self._log.info("Shutdown output") self._output_closed = True def _finalize(self, signal_name: str) -> None: 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) -> None: super().destroy() self._con = None self._client = None self._write_queue = None python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/third_party/000077500000000000000000000000001524361513300253115ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/third_party/__init__.py000066400000000000000000000000001524361513300274100ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/third_party/hsluv.py000066400000000000000000000172531524361513300270340ustar00rootroot00000000000000# This file was taken from https://github.com/hsluv/hsluv-python # # Copyright (c) 2015 Alexei Boronine # # SPDX-License-Identifier: MIT import math ColorT = tuple[float, float, float] # XYZ-to-sRGB matrix _m = [ [3.240969941904521, -1.537383177570093, -0.498610760293], [-0.96924363628087, 1.87596750150772, 0.041555057407175], [0.055630079696993, -0.20397695888897, 1.056971514242878], ] # sRGB-to-XYZ matrix _m_inv = [ [0.41239079926595, 0.35758433938387, 0.18048078840183], [0.21263900587151, 0.71516867876775, 0.072192315360733], [0.019330818715591, 0.11919477979462, 0.95053215224966], ] _ref_y = 1.0 _ref_u = 0.19783000664283 _ref_v = 0.46831999493879 _kappa = 903.2962962 # 24389/27 == (29/3)**3 _epsilon = 0.0088564516 # 216/24389 == (6/29)**3 def _normalize_output(_hx_tuple: ColorT) -> ColorT: # as in snapshot rev 4, the tolerance should be 1e-11 return ( round(_hx_tuple[0], ndigits=11 - 1), round(_hx_tuple[1], ndigits=11 - 1), round(_hx_tuple[2], ndigits=11 - 1), ) def _distance_line_from_origin(line: dict[str, float]) -> float: v = line["slope"] ** 2 + 1 return abs(line["intercept"]) / math.sqrt(v) def _length_of_ray_until_intersect(theta: float, line: dict[str, float]) -> float: return line["intercept"] / (math.sin(theta) - line["slope"] * math.cos(theta)) def _get_bounds(l: float) -> list[dict[str, float]]: result: list[dict[str, float]] = [] sub1 = ((l + 16) ** 3) / 1560896 if sub1 > _epsilon: sub2 = sub1 else: sub2 = l / _kappa _g = 0 while _g < 3: c = _g _g += 1 m1 = _m[c][0] m2 = _m[c][1] m3 = _m[c][2] _g1 = 0 while _g1 < 2: t = _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: float) -> float: return min(_distance_line_from_origin(bound) for bound in _get_bounds(l)) def _max_chroma_for_lh(l: float, h: float) -> float: hrad = math.radians(h) lengths = [_length_of_ray_until_intersect(hrad, bound) for bound in _get_bounds(l)] return min(length for length in lengths if length >= 0) def _dot_product(a: list[float], b: ColorT) -> float: return sum(i * j for i, j in zip(a, b, strict=True)) def _from_linear(c: float) -> float: if c <= 0.0031308: return 12.92 * c return 1.055 * math.pow(c, 5 / 12) - 0.055 def _to_linear(c: float) -> float: if c > 0.04045: return math.pow((c + 0.055) / 1.055, 2.4) return c / 12.92 def _y_to_l(y: float) -> float: if y <= _epsilon: return y / _ref_y * _kappa return 116 * math.pow(y / _ref_y, 1 / 3) - 16 def _l_to_y(l: float) -> float: if l <= 8: return _ref_y * l / _kappa return _ref_y * (((l + 16) / 116) ** 3) def xyz_to_rgb(_hx_tuple: ColorT) -> ColorT: 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: ColorT) -> ColorT: rgbl = ( _to_linear(_hx_tuple[0]), _to_linear(_hx_tuple[1]), _to_linear(_hx_tuple[2]), ) return ( _dot_product(_m_inv[0], rgbl), _dot_product(_m_inv[1], rgbl), _dot_product(_m_inv[2], rgbl), ) def xyz_to_luv(_hx_tuple: ColorT) -> ColorT: x = float(_hx_tuple[0]) y = float(_hx_tuple[1]) z = float(_hx_tuple[2]) l = _y_to_l(y) if l == 0: return (0, 0, 0) divider = x + 15 * y + 3 * z if divider == 0: u = v = float("nan") return (l, u, v) var_u = 4 * x / divider var_v = 9 * y / divider u = 13 * l * (var_u - _ref_u) v = 13 * l * (var_v - _ref_v) return (l, u, v) def luv_to_xyz(_hx_tuple: ColorT) -> ColorT: 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) + _ref_u var_v = v / (13 * l) + _ref_v y = _l_to_y(l) x = y * 9 * var_u / (4 * var_v) z = y * (12 - 3 * var_u - 20 * var_v) / (4 * var_v) return (x, y, z) def luv_to_lch(_hx_tuple: ColorT) -> ColorT: l = float(_hx_tuple[0]) u = float(_hx_tuple[1]) v = float(_hx_tuple[2]) c = math.hypot(u, v) if c < 1e-08: h = 0 else: hrad = math.atan2(v, u) h = math.degrees(hrad) if h < 0: h += 360 return (l, c, h) def lch_to_luv(_hx_tuple: ColorT) -> ColorT: l = float(_hx_tuple[0]) c = float(_hx_tuple[1]) h = float(_hx_tuple[2]) hrad = math.radians(h) u = math.cos(hrad) * c v = math.sin(hrad) * c return (l, u, v) def hsluv_to_lch(_hx_tuple: ColorT) -> ColorT: h = float(_hx_tuple[0]) s = float(_hx_tuple[1]) l = float(_hx_tuple[2]) if l > 100 - 1e-7: return (100, 0, h) if l < 1e-08: 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: ColorT) -> ColorT: l = float(_hx_tuple[0]) c = float(_hx_tuple[1]) h = float(_hx_tuple[2]) if l > 100 - 1e-7: return (h, 0, 100) if l < 1e-08: 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: ColorT) -> ColorT: h = float(_hx_tuple[0]) s = float(_hx_tuple[1]) l = float(_hx_tuple[2]) if l > 100 - 1e-7: return (100, 0, h) if l < 1e-08: 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: ColorT) -> ColorT: l = float(_hx_tuple[0]) c = float(_hx_tuple[1]) h = float(_hx_tuple[2]) if l > 100 - 1e-7: return (h, 0, 100) if l < 1e-08: 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: ColorT) -> str: return "#{:02x}{:02x}{:02x}".format( math.floor(_hx_tuple[0] * 255 + 0.5), math.floor(_hx_tuple[1] * 255 + 0.5), math.floor(_hx_tuple[2] * 255 + 0.5), ) def hex_to_rgb(_hex: str) -> ColorT: # skip leading '#' r = int(_hex[1:3], base=16) / 255.0 g = int(_hex[3:5], base=16) / 255.0 b = int(_hex[5:7], base=16) / 255.0 return (r, g, b) def lch_to_rgb(_hx_tuple: ColorT) -> ColorT: return xyz_to_rgb(luv_to_xyz(lch_to_luv(_hx_tuple))) def rgb_to_lch(_hx_tuple: ColorT) -> ColorT: return luv_to_lch(xyz_to_luv(rgb_to_xyz(_hx_tuple))) def _hsluv_to_rgb(_hx_tuple: ColorT) -> ColorT: return lch_to_rgb(hsluv_to_lch(_hx_tuple)) def hsluv_to_rgb(_hx_tuple: ColorT) -> ColorT: return _normalize_output(_hsluv_to_rgb(_hx_tuple)) def rgb_to_hsluv(_hx_tuple: ColorT) -> ColorT: return lch_to_hsluv(rgb_to_lch(_hx_tuple)) def _hpluv_to_rgb(_hx_tuple: ColorT) -> ColorT: return lch_to_rgb(hpluv_to_lch(_hx_tuple)) def hpluv_to_rgb(_hx_tuple: ColorT) -> ColorT: return _normalize_output(_hpluv_to_rgb(_hx_tuple)) def rgb_to_hpluv(_hx_tuple: ColorT) -> ColorT: return lch_to_hpluv(rgb_to_lch(_hx_tuple)) def hsluv_to_hex(_hx_tuple: ColorT) -> str: return rgb_to_hex(hsluv_to_rgb(_hx_tuple)) def hpluv_to_hex(_hx_tuple: ColorT) -> str: return rgb_to_hex(hpluv_to_rgb(_hx_tuple)) def hex_to_hsluv(s: str) -> ColorT: return rgb_to_hsluv(hex_to_rgb(s)) def hex_to_hpluv(s: str) -> ColorT: return rgb_to_hpluv(hex_to_rgb(s)) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/types.py000066400000000000000000000003361524361513300245000ustar00rootroot00000000000000from typing import Literal from nbxmpp.const import ConnectionProtocol from nbxmpp.const import ConnectionType BlockingReportValues = Literal["spam", "abuse"] CustomHostT = tuple[str, ConnectionProtocol, ConnectionType] python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/util.py000066400000000000000000000361521524361513300243160ustar00rootroot00000000000000# Copyright (C) 2018 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations from typing import Any from typing import Literal from typing import TYPE_CHECKING import base64 import binascii import hashlib import logging import os import re import uuid import xml.etree.ElementTree as ET from collections import defaultdict from collections.abc import Callable from functools import lru_cache from logging import LoggerAdapter from gi.repository import Gio from gi.repository import Soup from packaging.version import Version from nbxmpp.const import GIO_TLS_ERRORS from nbxmpp.const import GLIB_VERSION from nbxmpp.modules.dataforms import DataForm from nbxmpp.modules.dataforms import extend_form from nbxmpp.namespaces import Namespace from nbxmpp.protocol import DiscoInfoMalformed from nbxmpp.protocol import JID from nbxmpp.protocol import StanzaMalformed from nbxmpp.protocol import StreamHeader from nbxmpp.protocol import WebsocketOpenHeader from nbxmpp.simplexml import Node from nbxmpp.structs import CommonError from nbxmpp.structs import DiscoIdentity from nbxmpp.structs import DiscoInfo from nbxmpp.structs import HTTPUploadError from nbxmpp.structs import IqProperties from nbxmpp.structs import MessageProperties from nbxmpp.structs import PresenceProperties from nbxmpp.structs import Properties from nbxmpp.third_party import hsluv if TYPE_CHECKING: from nbxmpp.protocol import Protocol log = logging.getLogger("nbxmpp.util") SHA1_RX = re.compile(r"[a-f0-9]{40}") def b64decode(data: str | bytes) -> bytes: if not data: raise ValueError("No data to decode") if isinstance(data, str): data = data.encode() # Don't validate base64 data because in XMPP whitespace in # base64 payloads must be accepted return base64.b64decode(data) def b64encode(data: str | bytes) -> str: if not data: raise ValueError("No data to encode") if isinstance(data, str): data = data.encode() result = base64.b64encode(data) return result.decode() def get_properties_struct( name: str, own_jid: JID ) -> MessageProperties | IqProperties | PresenceProperties | Properties: 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: str) -> bool: 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: bool | None) -> Literal["true", "false"]: # 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: Protocol) -> Any: app_namespace = stanza.getAppErrorNamespace() return error_classes.get(app_namespace, CommonError)(stanza) def clip_rgb(red: float, green: float, blue: float) -> tuple[float, float, float]: return ( min(max(red, 0), 1), min(max(green, 0), 1), min(max(blue, 0), 1), ) def text_to_hue(text: str) -> float: hash_ = hashlib.sha1() hash_.update(text.encode()) return int.from_bytes(hash_.digest()[:2], "little") / 65536 * 360 def hsluv_to_rgb( hue: float, saturation: float, lightness: float, ) -> tuple[float, float, float]: return clip_rgb(*hsluv.hsluv_to_rgb((hue, saturation, lightness))) @lru_cache(maxsize=1024) def text_to_color( text: str, saturation: float, lightness: float, ) -> tuple[float, float, float]: """ Combines algorithms from XEP-0392 (version 0.8.0 ): 5.1 Angle generation, 5.4 RGB generation. Input: text: the subject to compute hue for, saturation ∈ [0, 100], lightness ∈ [0, 100]. Returns color in sRGB color space: r, g, b ∈ [0, 1]. """ return hsluv_to_rgb(text_to_hue(text), saturation, lightness) def compute_caps_hash(info: DiscoInfo, compare: bool = True) -> str: """ 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: DiscoIdentity) -> tuple[str, str, str]: 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: list[DataForm] = [] form_type_values: list[Any] = [] 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: 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() -> str: return str(uuid.uuid4()) def get_form(stanza: Node, form_type: Any) -> Any: 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: Node, domain: str, is_websocket: bool) -> str: 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: str, lang: str, is_websocket: bool) -> str: if is_websocket: return str(WebsocketOpenHeader(domain, lang)) header = StreamHeader(domain, lang) return "%s>" % str(header)[:-3] def utf8_decode(data: bytes) -> tuple[str, bytes]: """ 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() -> int: return int(binascii.hexlify(os.urandom(6)), 16) def get_invalid_xml_regex() -> re.Pattern[str]: # \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) INVALID_XML_RX = get_invalid_xml_regex() def get_tls_error_phrase(tls_error: Gio.TlsCertificateFlags) -> str | None: 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: Gio.TlsCertificateFlags | None, ) -> set[Gio.TlsCertificateFlags]: 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: Soup.WebsocketConnection) -> str: 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: Node) -> bool: return stanza.getName() == "close" and stanza.getNamespace() == Namespace.FRAMING def is_websocket_stream_error(stanza: Node) -> bool: return stanza.getName() == "error" and stanza.getNamespace() == Namespace.STREAMS def min_version(name: str, min_version: str) -> bool: if name == "GLib": return GLIB_VERSION >= Version(min_version) raise ValueError("Unknown library name") class Observable: def __init__(self, log_: logging.Logger | LogAdapter) -> None: self._log = log_ self._frozen = False self._callbacks: defaultdict[str, list[Callable[..., Any]]] = defaultdict(list) def remove_subscriptions(self) -> None: self._callbacks = defaultdict(list) def subscribe(self, signal_name: str, func: Callable[..., Any]) -> None: self._callbacks[signal_name].append(func) def notify(self, signal_name: str, *args: Any, **kwargs: dict[str, Any]) -> None: 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: str) -> None: self.extra["context"] = context def process(self, msg: str, kwargs: Any) -> str: return "(%s) %s" % (self.extra["context"], msg), kwargs def parse_websocket_uri(data: str) -> str: """ Example: """ host_meta_xml = ET.fromstring(data) for link in host_meta_xml.findall(f"{{{Namespace.XRD}}}Link"): if link.attrib.get("rel") == "urn:xmpp:alt-connections:websocket": href = link.attrib.get("href") if href is None: raise ValueError("No href attr found") return href raise ValueError("no websocket uri found") def normalize_sha1(value: str) -> str | None: """Return a canonical lowercase SHA-1 hex digest, or None if invalid.""" value = value.lower() if SHA1_RX.fullmatch(value) is None: return None return value python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/websocket.py000066400000000000000000000166361524361513300253340ustar00rootroot00000000000000# Copyright (C) 2020 Philipp Hörist # # This file is part of nbxmpp. # # SPDX-License-Identifier: GPL-3.0-or-later from typing import Any import logging from gi.repository import Gio from gi.repository import GLib from gi.repository import Soup import nbxmpp from nbxmpp.connection import Connection from nbxmpp.const import TCPState from nbxmpp.protocol import Protocol from nbxmpp.util import convert_tls_error_flags from nbxmpp.util import get_websocket_close_string log = logging.getLogger("nbxmpp.websocket") class WebsocketConnection(Connection): def __init__(self, *args: Any, **kwargs: Any) -> None: Connection.__init__(self, *args, **kwargs) self._session = Soup.Session() self._session.set_user_agent(f"nbxmpp/{nbxmpp.__version__}") if self._log.getEffectiveLevel() == logging.INFO: self._session.add_feature(Soup.Logger.new(Soup.LoggerLogLevel.BODY)) self._websocket: Soup.WebsocketConnection | None = None self._cancellable = Gio.Cancellable() self._input_closed = False self._output_closed = False def connect(self) -> None: assert self._address is not None self._log.info("Try to connect to %s", self._address.uri) self.state = TCPState.CONNECTING message = Soup.Message.new("GET", self._address.uri) assert message is not None message.connect("accept-certificate", self._check_certificate) message.connect("notify::tls-peer-certificate", self._on_certificate_set) message.connect("network-event", self._on_network_event) message.set_flags(Soup.MessageFlags.NO_REDIRECT) assert self._session is not None self._session.websocket_connect_async( message, None, ["xmpp"], GLib.PRIORITY_DEFAULT, self._cancellable, self._on_connect, None, ) def _on_connect( self, session: Soup.Session, result: Gio.AsyncResult, _user_data: None ) -> None: # 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.set_max_incoming_payload_size(1048576) 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) -> None: # Soup.Session does this automatically raise NotImplementedError def _check_certificate( self, _message: Soup.Message, certificate: Gio.TlsCertificate, errors: Gio.TlsCertificateFlags, ) -> bool: self._peer_certificate = certificate self._peer_certificate_errors = convert_tls_error_flags(errors) if self._accept_certificate(): return True self.notify("bad-certificate") self._cancellable.cancel() return False def _on_network_event( self, _message: Soup.Message, event: Gio.SocketClientEvent, connection: Gio.TlsConnection, ) -> None: if event == Gio.SocketClientEvent.TLS_HANDSHAKED: self._tls_con = connection def _on_certificate_set(self, message: Soup.Message, _param: Any) -> None: if self._peer_certificate is not None: return # If the cert has errors _check_certificate() will set the cert. self._peer_certificate = message.props.tls_peer_certificate self._peer_certificate_errors = convert_tls_error_flags( message.props.tls_peer_certificate_errors ) def _on_websocket_message( self, _websocket: Soup.WebsocketConnection, _type: int, message: GLib.Bytes ) -> None: 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: Soup.WebsocketConnection, _message: GLib.Bytes ) -> None: self._log.info("Pong received") def _on_websocket_closed(self, websocket: Soup.WebsocketConnection) -> None: self._log.info("Closed %s", get_websocket_close_string(websocket)) self._finalize("disconnected") def _on_websocket_closing(self, _websocket: Soup.WebsocketConnection) -> None: self._log.info("Closing") def _on_websocket_error( self, _websocket: Soup.WebsocketConnection, error: GLib.Error ) -> None: self._log.error(error) if self._state not in (TCPState.DISCONNECTED, TCPState.DISCONNECTING): self._finalize("disconnected") def send(self, stanza: Protocol, now: bool = False) -> None: 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) -> None: 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) -> None: if self._input_closed and self._output_closed: self._websocket.close(Soup.WebsocketCloseCode.NORMAL, None) def shutdown_input(self) -> None: self._log.info("Shutdown input") self._input_closed = True self._check_for_shutdown() def shutdown_output(self) -> None: self.state = TCPState.DISCONNECTING self._log.info("Shutdown output") self._output_closed = True def _finalize(self, signal_name: str) -> None: if self._websocket is not None: self._websocket.disconnect_by_func(self._on_websocket_message) self._websocket.disconnect_by_func(self._on_websocket_closed) self._websocket.disconnect_by_func(self._on_websocket_closing) self._websocket.disconnect_by_func(self._on_websocket_error) self._websocket.disconnect_by_func(self._on_websocket_pong) self._input_closed = True self._output_closed = True self.state = TCPState.DISCONNECTED self.notify(signal_name) self.destroy() def destroy(self) -> None: super().destroy() self._session.abort() self._session = None self._websocket = None python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/nbxmpp/xmppiri.py000066400000000000000000000065721524361513300250340ustar00rootroot00000000000000from __future__ import annotations import functools import re from collections.abc import Callable # https://www.rfc-editor.org/rfc/rfc3987 ucschar = ( "\xA0-\uD7FF" "\uF900-\uFDCF" "\uFDF0-\uFFEF" "\U00010000-\U0001FFFD" "\U00020000-\U0002FFFD" "\U00030000-\U0003FFFD" "\U00040000-\U0004FFFD" "\U00050000-\U0005FFFD" "\U00060000-\U0006FFFD" "\U00070000-\U0007FFFD" "\U00080000-\U0008FFFD" "\U00090000-\U0009FFFD" "\U000A0000-\U000AFFFD" "\U000B0000-\U000BFFFD" "\U000C0000-\U000CFFFD" "\U000D0000-\U000DFFFD" "\U000E1000-\U000EFFFD" ) ALPHA = "A-Za-z" DIGIT = "0-9" unreserved = rf"{ALPHA}{DIGIT}\-\._\~" subdelims = "!$&'()*+,;=" iunreserved = f"{unreserved}{ucschar}" ipchar = f"{iunreserved}{re.escape(subdelims)}:@" ifragment = rf"{ipchar}/\?" # https://www.rfc-editor.org/rfc/rfc5122.html#section-2.2 nodeallow = r"!$()*+,;=" resallow = r"!$&'()*+,:;=" inode = f"{iunreserved}{re.escape(nodeallow)}" ires = f"{iunreserved}{re.escape(resallow)}" ivalue = f"{iunreserved}" rx_iunreserved = re.compile(f"[{iunreserved}]*") rx_inode = re.compile(f"[{inode}]") rx_ires = re.compile(f"[{ires}]") rx_ikey = rx_iunreserved rx_iquerytype = rx_iunreserved rx_ivalue = rx_iunreserved rx_ifragment = re.compile(f"[{ifragment}]") class _Quoter(dict[str, str]): """A mapping from a string to its percent encoded form. Mapping is only done if string is not in safe range. Keeps a cache internally, via __missing__, for efficiency (lookups of cached keys don't call Python code at all). """ def __init__(self, safe: re.Pattern[str]) -> None: self._safe = safe def __repr__(self) -> str: return f"" def __missing__(self, b: str) -> str: if len(b) != 1: raise ValueError("String must be exactly one character long") if self._safe.fullmatch(b) is None: res = "".join(["%{:02X}".format(i) for i in b.encode()]) else: res = b self[b] = res return res @functools.lru_cache def _quoter_factory(safe: re.Pattern[str]) -> Callable[[str], str]: return _Quoter(safe).__getitem__ def validate_ikey(ikey: str) -> str: res = rx_ikey.fullmatch(ikey) if res is None: raise ValueError("Not allowed characters in key") return ikey def validate_querytype(querytype: str) -> str: res = rx_iquerytype.fullmatch(querytype) if res is None: raise ValueError("Not allowed characters in querytype") return querytype def _escape(string: str, pattern: re.Pattern[str]) -> str: quoter = _quoter_factory(safe=pattern) return "".join([quoter(c) for c in string]) def escape_ifragment(fragment: str) -> str: return _escape(fragment, rx_ifragment) def escape_ivalue(value: str) -> str: return _escape(value, rx_ivalue) def escape_inode(node: str) -> str: return _escape(node, rx_inode) def escape_ires(res: str) -> str: return _escape(res, rx_ires) def clean_iri(iri_str: str) -> str: if not iri_str.startswith("xmpp:"): raise ValueError("IRI must start with xmpp scheme") iri_str = iri_str.removeprefix("xmpp:") if iri_str.startswith("//"): raise ValueError("IRI with auth component is unsupported") # Remove query and fragment iri_str = iri_str.split("?", maxsplit=1)[0] iri_str = iri_str.split("#", maxsplit=1)[0] return iri_str python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/pyproject.toml000066400000000000000000000166021524361513300243750ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 65.0.0"] build-backend = "setuptools.build_meta" [project] name = "nbxmpp" description = "XMPP Library" keywords = ["chat", "messaging", "im", "xmpp"] readme = "README.md" requires-python = ">=3.10" license = "GPL-3.0-or-later" authors = [ {name = "Philipp Hörist", email = "philipp@hoerist.com"}, {name = "Yann Leboulanger", email = "yann@leboulanger.org"}, ] classifiers = [ "Programming Language :: Python :: 3.10", ] dependencies = [ "precis-i18n>=1.0.0", "packaging", "idna", "PyGObject>=3.42.0", ] dynamic = ["version"] [dependency-groups] gssapi = ["gssapi"] dev = [ "black==24.10.0", "isort>=7.0.0", "pre-commit", "pygobject-stubs@git+https://github.com/pygobject/pygobject-stubs.git", "ruff==0.14.8", ] [project.urls] repository = "https://dev.gajim.org/gajim/python-nbxmpp" [tool.setuptools.packages.find] include = ["nbxmpp*"] [tool.setuptools.package-data] "nbxmpp" = ["py.typed"] [tool.setuptools.dynamic] version = {attr = "nbxmpp.__version__"} [tool.pyright] pythonVersion = "3.10" pythonPlatform = "All" typeCheckingMode = "strict" reportUnnecessaryTypeIgnoreComment = "error" reportPropertyTypeMismatch = "error" reportMissingModuleSource = "none" exclude = [ "**/__pycache__", ".git", ".venv", "build", "nbxmpp/modules", "nbxmpp/third_party/hsluv.py", "nbxmpp/addresses.py", "nbxmpp/client.py", "nbxmpp/const.py", "nbxmpp/dispatcher.py", "nbxmpp/errors.py", "nbxmpp/http.py", "nbxmpp/old_dispatcher.py", "nbxmpp/plugin.py", "nbxmpp/protocol.py", "nbxmpp/sasl.py", "nbxmpp/simplexml.py", "nbxmpp/smacks.py", "nbxmpp/structs.py", "nbxmpp/tcp.py", "nbxmpp/util.py", "nbxmpp/websocket.py", "test" ] include = [ "nbxmpp/modules/base.py", "nbxmpp/modules/bits_of_binary.py", "nbxmpp/modules/chat_markers.py", "nbxmpp/modules/correction.py", "nbxmpp/modules/date_and_time.py", "nbxmpp/modules/fallback.py", "nbxmpp/modules/http_auth.py", "nbxmpp/modules/pgplegacy.py", "nbxmpp/modules/reactions.py", "nbxmpp/modules/replies.py", "nbxmpp/modules/vcard_avatar.py" ] [tool.black] line-length = 88 target-version = ["py310", "py311", "py312"] required-version = "24.10.0" [tool.isort] force_alphabetical_sort_within_sections = true force_single_line = true group_by_package = true known_typing = ["typing"] sections = [ "FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER" ] skip_gitignore = true [tool.ruff] line-length = 88 target-version = "py310" [tool.ruff.lint] select = [ "A", # flake8-builtins # "AIR", # Airflow # "ANN", # flake8-annotations # "ARG", # flake8-unused-arguments # "ASYNC", # flake8-async "B", # flake8-bugbear "BLE", # flake8-blind-except "C4", # flake8-comprehensions "C90", # mccabe "COM", # flake8-commas # "CPY", # flake8-copyright # "D", # pydocstyle # "DJ", # flake8-django # "DTZ", # flake8-datetimez "E", # Error # "EM", # flake8-errmsg # "ERA", # eradicate "EXE", # flake8-executable "F", # Pyflakes "FA", # flake8-future-annotations # "FBT", # flake8-boolean-trap # "FIX", # flake8-fixme # "FLY", # flynt "G", # flake8-logging-format # "I", # isort "ICN", # flake8-import-conventions # "INP", # flake8-no-pep420 "INT", # flake8-gettext # "ISC", # flake8-implicit-str-concat "N", # pep8-naming # "NPY", # NumPy-specific rules # "PD", # pandas-vet # "PERF", # Perflint "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # Pylint # "PT", # flake8-pytest-style # "PTH", # flake8-use-pathlib # "PYI", # flake8-pyi # "Q", # flake8-quotes # "RET", # flake8-return "RSE", # flake8-raise "RUF", # Ruff-specific rules "S", # flake8-bandit "SIM", # flake8-simplify # "SLF", # flake8-self "SLOT", # flake8-slots "T10", # flake8-debugger # "T20", # flake8-print # "TCH", # flake8-type-checking # "TD", # flake8-todos # "TID", # flake8-tidy-imports # "TRY", # tryceratops "UP", # pyupgrade "W", # Warning "YTT", # flake8-2020 ] ignore = [ "A003", # Class attribute is shadowing python builtin "A005", # Module shadows a Python standard-library module "B904", # Within an except clause .. "BLE001", # Blind except Exception: statement "C901", # too complex "COM812", # Trailing comma missing "E501", # Line too long "E741", # Ambiguous variable name "FBT003", # Boolean positional value in function call "N801", # Class name `x` should use CapWords convention "N802", # Function name should be lower case "N806", # Variable should be lower case "N817", # CamelCase imported as acronym "N818", # Exception name should be named with an Error suffix "PGH003", # Use specific rule codes when ignoring type issues "PLC1901",# Expression can be simplified to x as an empty string is falsey "PLR0911",# Too many return statements "PLR0912",# Too many branches "PLR0913",# Too many arguments to function call "PLR0915",# Too many statements "PLR2004",# Magic value used in comparison, consider replacing x with a constant variable "PLR5501",# Consider using `elif` instead of `else` then `if` to remove one indentation level "PLW0603",# Using the global statement to update `x` is discouraged "PLW1641",# Object does not implement `__hash__` method "PLW2901",# `for` loop variable `x` overwritten by assignment target "RUF001", # AmbiguousUnicodeCharacterString "RUF002", # AmbiguousUnicodeCharacterDocstring "RUF003", # AmbiguousUnicodeCharacterComment "RUF005", # Consider x instead of concatenation "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` "RUF100", # Unused `noqa` directive "S101", # Use of `assert` detected "S110", # `try`-`except`-`pass` detected, consider logging the exception "S112", # try`-`except`-`continue` "S303", # Use of insecure MD2, MD4, MD5, or SHA1 hash function "S310", # Audit URL open for permitted schemes "S314", # Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents "S324", # Probable use of insecure hash functions "S608", # Possible SQL injection vector through string-based query construction "SIM102", # Use a single `if` statement instead of nested `if` statements "SIM105", # Use `contextlib.suppress(Exception)` instead of try-except-pass "SIM108", # Use ternary operator "SIM114", # Combine `if` branches using logical `or` operator" "SIM115", # Use context handler for opening files "SIM201", # Use x instead of not x "SIM212", # Use `value if value else ''` instead of `'' if not value else value` "SIM300", # Yoda conditions are discouraged use x instead "UP037", # Remove quotes from type annotation "UP031", # Use format specifiers instead of percent format "UP032", # Use f-string instead of `format` call "UP007", # Use `X | Y` for type annotations ] exclude = [ ".eggs", ".git", ".ruff_cache", ".venv", "build", "dist", "venv", "*.pyi", ] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.lint.per-file-ignores] "nbxmpp/old_dispatcher.py" = ["B011", "N803"] [tool.ruff.lint.mccabe] max-complexity = 15 python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/python-nbxmpp.doap000066400000000000000000000433631524361513300251550ustar00rootroot00000000000000 python-nbxmpp nbxmpp 2012-05-08 Python XMPP 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. Python Linux Windows complete 2.9 complete 1.3 complete 2.4 complete 1.31.2 All basics stuff is supported. Also, some more advanced features like invitations, creation of rooms, etc. complete 1.3 complete 1.1 complete 1.2 complete 1.2 complete 1.0 Used by XEP-0313. partial 1.13 partial 1.5 complete 1.0 complete 2.4 complete 1.9 complete 1.1 complete 1.0 complete 1.1.1 complete 2.1 complete 1.1 complete 1.1.1 complete 1.2 complete 1.3 complete 1.5 complete 1.2 complete 1.0 complete 1.0 partial 1.0 Images only. complete 1.0 complete 1.2 complete 1.2 complete 1.3 complete 2.0 complete 1.0 partial 1.0 No cache: Only for captchas. complete 1.0.0 complete 1.0 complete 0.12.0 complete 0.10 complete 1.0 complete 0.6.3 partial Supports reading hats, but not setting hats 0.3.1 complete 1.0.2 complete 0.2.1 complete 0.7.0 complete 1.0.0 complete 0.4.0 partial 0.2 Basic Spam/Abuse reporting via XEP-0191 complete 0.3.3 complete 0.1 complete 0.3.0 partial 0.3.0 No support for 'continue' flows complete 0.8.0 complete 0.3.0 complete 0.1.0 complete 0.4.0 complete 0.3.0 complete 0.2.1 complete 0.4.0 complete 0.1.0 complete 0.2.0 complete 0.2.0 complete 0.1.0 complete 0.1.0 7.1.0 python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/scripts/000077500000000000000000000000001524361513300231435ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/scripts/bump_version.py000077500000000000000000000026511524361513300262340ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import re import subprocess import sys from pathlib import Path REPO_DIR = Path(__file__).resolve().parent.parent INIT = REPO_DIR / "nbxmpp" / "__init__.py" CHANGELOG = REPO_DIR / "ChangeLog" VERSION_RX = r"\d+\.\d+\.\d+" def get_current_version() -> str: content = INIT.read_text(encoding="utf8") match = re.search(VERSION_RX, content) if match is None: sys.exit("Unable to find current version") return match[0] def bump_version(current_version: str, new_version: str) -> None: content = INIT.read_text(encoding="utf8") content = content.replace(current_version, new_version, 1) INIT.write_text(content, encoding="utf8") def make_changelog(new_version: str) -> None: cmd = ["git-chglog", "--next-tag", new_version] result = subprocess.run( # noqa: S603 cmd, cwd=REPO_DIR, text=True, check=True, capture_output=True ) changes = result.stdout changes = changes.removeprefix("\n") current_changelog = CHANGELOG.read_text() with CHANGELOG.open("w") as f: f.write(changes + current_changelog) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Bump Version") parser.add_argument("version", help="The new version, e.g. 1.5.0") args = parser.parse_args() current_version = get_current_version() bump_version(current_version, args.version) make_changelog(args.version) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/000077500000000000000000000000001524361513300224335ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/__init__.py000066400000000000000000000001641524361513300245450ustar00rootroot00000000000000import logging # Prevents logging output in tests log = logging.getLogger("nbxmpp") log.setLevel(logging.CRITICAL) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/lib/000077500000000000000000000000001524361513300232015ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/lib/__init__.py000066400000000000000000000015121524361513300253110ustar00rootroot00000000000000def 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 += "" return s python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/lib/const.py000066400000000000000000000001471524361513300247030ustar00rootroot00000000000000STREAM_START = "" python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/lib/util.py000066400000000000000000000026031524361513300245310ustar00rootroot00000000000000import datetime as dt import sys import unittest from test.lib.const import STREAM_START from unittest.mock import Mock 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) def raise_all_exceptions(func): # Exceptions which are raised from async callbacks # in GLib or GTK do not bubble up to the unittest # This decorator catches all exceptions and raises them # after the unittest def func_wrapper(self, *args, **kwargs): exceptions = [] def on_hook(type_, value, tback): exceptions.append((type_, value, tback)) orig_excepthook = sys.excepthook sys.excepthook = on_hook try: result = func(self) finally: sys.excepthook = orig_excepthook if exceptions: tp, value, tb = exceptions[0] raise tp(value).with_traceback(tb) return result return func_wrapper def utc_now() -> dt.datetime: return dt.datetime.now(dt.UTC) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/lib/xmpp_mocks.py000066400000000000000000000023241524361513300257340ustar00rootroot00000000000000""" Module with dummy classes for unit testing of XMPP and related code. """ from unittest.mock import Mock class MockConnection(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 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 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 python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/000077500000000000000000000000001524361513300234125ustar00rootroot00000000000000python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/__init__.py000066400000000000000000000000661524361513300255250ustar00rootroot00000000000000""" This package just contains plain unit tests """ python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_activity.py000066400000000000000000000036251524361513300266650ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.namespaces import Namespace from nbxmpp.structs import ActivityData from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_avatar.py000066400000000000000000000044421524361513300263050ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.modules.user_avatar import AvatarInfo from nbxmpp.modules.user_avatar import AvatarMetaData from nbxmpp.namespaces import Namespace from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_body.py000066400000000000000000000040431524361513300257610ustar00rootroot00000000000000from __future__ import annotations from test.lib.util import StanzaHandlerTest import nbxmpp from nbxmpp.language import LanguageRange from nbxmpp.structs import BodyData class TestBody(StanzaHandlerTest): def test_body_language(self): text = "> Anna wrote:\n> Hi, how are you?\nGreat" text_de = "> Anna schrieb\n>Hallo, wie geht es dir?\nGut" xml = """ %s %s %s """ % ( text, text, text_de, ) message = nbxmpp.Message(node=xml) body_data = BodyData(message, None, None) body = body_data.get([LanguageRange(tag="en")]) self.assertEqual(body, text) body = body_data.get(None) self.assertEqual(body, text) body = body_data.get([LanguageRange(tag="de")]) self.assertEqual(body, text_de) body = body_data.get([LanguageRange(tag="ar")]) self.assertEqual(body, text) xml = """ en de """ message = nbxmpp.Message(node=xml) body_data = BodyData(message) body = body_data.get([LanguageRange(tag="en")]) self.assertEqual(body, "en") # Language not in body, pick any language body_data = BodyData(message) body = body_data.get([LanguageRange(tag="ar")]) self.assertEqual(body, "de") # Supply range with priority body_data = BodyData(message) body = body_data.get([LanguageRange(tag="de"), LanguageRange(tag="en")]) self.assertEqual(body, "de") body_data = BodyData(message) body = body_data.get([LanguageRange(tag="ar"), LanguageRange(tag="en")]) self.assertEqual(body, "en") python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_bookmarks.py000066400000000000000000000105531524361513300270170ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.namespaces import Namespace from nbxmpp.protocol import JID from nbxmpp.structs import BookmarkData from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler 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", # noqa: S106 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 = """ pass JC """ 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_datetime_parsing.py000066400000000000000000000113661524361513300303510ustar00rootroot00000000000000import unittest from datetime import datetime from datetime import timedelta from datetime import timezone from nbxmpp.modules.date_and_time import create_tzinfo from nbxmpp.modules.date_and_time import LocalTimezone from nbxmpp.modules.date_and_time import parse_datetime 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, "2017-11-05T01:41:20-00:00": 1509846080.0, "2017-11-05T01:41:20+00:00": 1509846080.0, } strings2 = { # Valid strings with offset "2017-11-05T07:41:20-05:00": datetime( 2017, 11, 5, 12, 41, 20, 0, timezone.utc ), "2017-11-05T07:41:20+05:00": datetime( 2017, 11, 5, 2, 41, 20, 0, timezone.utc ), "2017-11-05T01:41:20+00:00": datetime( 2017, 11, 5, 1, 41, 20, 0, timezone.utc ), "2017-11-05T01:41:20Z": datetime(2017, 11, 5, 1, 41, 20, 0, timezone.utc), "0002-11-05T01:41:20Z": datetime(2, 11, 5, 1, 41, 20, 0, timezone.utc), "9998-11-05T01:41:20Z": datetime(9998, 11, 5, 1, 41, 20, 0, timezone.utc), "0001-11-05T01:41:20Z": None, "9999-11-05T01:41:20Z": None, } 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) 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) assert result is not 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_delay_parsing.py000066400000000000000000000017661524361513300276560ustar00rootroot00000000000000import unittest from nbxmpp.modules.delay import parse_delay from nbxmpp.protocol import Node 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_entity_caps.py000066400000000000000000000210271524361513300273470ustar00rootroot00000000000000import unittest from nbxmpp.modules.discovery import parse_disco_info from nbxmpp.protocol import DiscoInfoMalformed from nbxmpp.protocol import Iq from nbxmpp.util import compute_caps_hash class EntityCaps(unittest.TestCase): def test_multiple_field_values(self): node = """ urn:xmpp:dataforms:softwareinfo ipv4 ipv6 Mac 10.5.1 Psi 0.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:softwareinfo ipv4 ipv6 Mac 10.5.1 Psi 0.11 urn:xmpp:dataforms:softwareinfo ipv4 ipv6 ipv4 ipv6 """ 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:softwareinfo urn:xmpp:dataforms:softwareinfo_test ipv4 ipv6 """ info = parse_disco_info(Iq(node=node)) with self.assertRaises(DiscoInfoMalformed): compute_caps_hash(info) def test_non_unique_form_type_value(self): node = """ urn:xmpp:dataforms:softwareinfo ipv4 ipv6 urn:xmpp:dataforms:softwareinfo ipv4 ipv6 """ info = parse_disco_info(Iq(node=node)) with self.assertRaises(DiscoInfoMalformed): compute_caps_hash(info) def test_non_unique_feature(self): node = """ """ info = parse_disco_info(Iq(node=node)) with self.assertRaises(DiscoInfoMalformed): compute_caps_hash(info) def test_non_unique_identity(self): node = """ """ info = parse_disco_info(Iq(node=node)) with self.assertRaises(DiscoInfoMalformed): compute_caps_hash(info) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_error_parsing.py000066400000000000000000000026311524361513300277010ustar00rootroot00000000000000import 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 bytes File zu groß. Erlaubt sind 20000 bytes 20000 """ 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_fallback.py000066400000000000000000000152251524361513300265670ustar00rootroot00000000000000from __future__ import annotations from test.lib.util import StanzaHandlerTest from unittest.mock import MagicMock import nbxmpp from nbxmpp.exceptions import FallbackLanguageError from nbxmpp.language import LanguageRange from nbxmpp.modules.fallback import FallbackRange from nbxmpp.modules.fallback import FallbacksForT from nbxmpp.modules.fallback import parse_fallback_indication from nbxmpp.modules.fallback import strip_fallback from nbxmpp.structs import BodyData class TestFallback(StanzaHandlerTest): def test_parse_fallback_indication(self): xml = """ > Anna wrote:\n> Hi, how are you?\nGreat """ log = MagicMock() message = nbxmpp.Message(node=xml) fallbacks_for = parse_fallback_indication(log, message) expected = { "urn:xmpp:test:0": { None: FallbackRange(0, 33), "en": FallbackRange(0, 32), "de": FallbackRange(0, 31), }, "urn:xmpp:test:1": { None: None, }, "urn:xmpp:test:2": None, } self.assertEqual(fallbacks_for, expected) xml = """ > Anna wrote:\n> Hi, how are you?\nGreat """ message = nbxmpp.Message(node=xml) fallbacks_for = parse_fallback_indication(log, message) self.assertIsNone(fallbacks_for) xml = """ > Anna wrote:\n> Hi, how are you?\nGreat """ message = nbxmpp.Message(node=xml) fallbacks_for = parse_fallback_indication(log, message) self.assertIsNone(fallbacks_for) def test_strip_fallback(self): fallbacks_for: FallbacksForT = { "urn:xmpp:test:1": { None: FallbackRange(33, 38), }, "urn:xmpp:test:0": { None: FallbackRange(0, 14), "en": FallbackRange(0, 33), }, "urn:xmpp:test:2": { None: FallbackRange(54, 55), "en": None, }, } text = "> Anna wrote:\n> Hi, how are you?\nGreat" # Strip one fallback with unavailable language with self.assertRaises(FallbackLanguageError): stripped_text = strip_fallback( fallbacks_for, {"urn:xmpp:test:0"}, "xx", text ) # Strip one fallback with language stripped_text = strip_fallback(fallbacks_for, {"urn:xmpp:test:0"}, "en", text) self.assertEqual(stripped_text, "Great") # Strip one fallback with no language stripped_text = strip_fallback(fallbacks_for, {"urn:xmpp:test:0"}, None, text) self.assertEqual(stripped_text, "> Hi, how are you?\nGreat") # Strip multiple fallbacks in correct order stripped_text = strip_fallback( fallbacks_for, {"urn:xmpp:test:0", "urn:xmpp:test:1"}, None, text ) self.assertEqual(stripped_text, "> Hi, how are you?\n") # Range out of bounds, don’t strip anything stripped_text = strip_fallback(fallbacks_for, {"urn:xmpp:test:2"}, None, text) self.assertEqual(stripped_text, text) # One Range is None, so whole body is fallback stripped_text = strip_fallback( fallbacks_for, {"urn:xmpp:test:0", "urn:xmpp:test:2"}, "en", text ) self.assertEqual(stripped_text, "") text = "> Short text" # One Range is bigger then the text, ignore fallback stripped_text = strip_fallback(fallbacks_for, {"urn:xmpp:test:0"}, None, text) self.assertEqual(stripped_text, text) def test_body_with_fallback(self): text = "> Anna wrote:\n> Hi, how are you?\nGreat" text_de = "> Anna schrieb\n>Hallo, wie geht es dir?\nGut" xml = """ %s %s %s """ % ( text, text, text_de, ) log = MagicMock() message = nbxmpp.Message(node=xml) fallbacks_for = parse_fallback_indication(log, message) body_data = BodyData(message, fallbacks_for, {"urn:xmpp:test:0"}) body = body_data.get([LanguageRange(tag="en")]) self.assertEqual(body, "Great") body = body_data.get(None) self.assertEqual(body, "Great") body = body_data.get([LanguageRange(tag="de")]) self.assertEqual(body, "Gut") body_data = BodyData(message, fallbacks_for, {"urn:xmpp:test:1"}) # specific language body requested # but there is no fallback for this language body = body_data.get([LanguageRange(tag="en")]) self.assertEqual(body, text) body = body_data.get(None) self.assertEqual(body, "") body_data = BodyData(message, fallbacks_for, {"urn:xmpp:test:2"}) # specific language body requested # fallback is for all bodies body = body_data.get([LanguageRange(tag="en")]) self.assertEqual(body, "") body = body_data.get(None) self.assertEqual(body, "") python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_hats.py000066400000000000000000000041011524361513300257560ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp import Presence from nbxmpp.language import LanguageRange from nbxmpp.modules.muc.hats import Hats from nbxmpp.structs import Hat from nbxmpp.structs import PresenceProperties class TestHats(StanzaHandlerTest): def test_parse_hats(self): # language=XML xml = """ """ props = PresenceProperties("mockjid") presence = Presence(node=xml) Hats._process_hats(None, None, presence, props) hat_list_en = [ Hat(uri="http://schemas.example.com/hats#member", title="Member"), Hat(uri="http://schemas.example.com/hats#owner", title="Owner"), ] hat_list_de = [ Hat(uri="http://schemas.example.com/hats#member", title="Mitglied"), Hat(uri="http://schemas.example.com/hats#owner", title="Eigentümer"), ] lang_range_en = [LanguageRange(tag="en")] lang_range_de = [LanguageRange(tag="de")] assert props.hats is not None hats_en = props.hats.get_hats(lang_range_en) self.assertSequenceEqual(hat_list_en, hats_en) hats_de = props.hats.get_hats(lang_range_de) self.assertSequenceEqual(hat_list_de, hats_de) hats_any = props.hats.get_hats() self.assertSequenceEqual(hat_list_de, hats_any) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_jid_parsing.py000066400000000000000000000235411524361513300273210ustar00rootroot00000000000000import os import unittest from nbxmpp.protocol import DomainpartByteLimit from nbxmpp.protocol import DomainpartNotAllowedChar from nbxmpp.protocol import InvalidJid from nbxmpp.protocol import JID from nbxmpp.protocol import LocalpartByteLimit from nbxmpp.protocol import LocalpartNotAllowedChar from nbxmpp.protocol import ResourcepartByteLimit from nbxmpp.protocol import ResourcepartNotAllowedChar from nbxmpp.protocol import validate_resourcepart 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), ("@example.com", LocalpartByteLimit), ("user@example.com/", ResourcepartByteLimit), ("user@example.com/\U00000001", ResourcepartNotAllowedChar), ("user@host@example.com", DomainpartNotAllowedChar), ("juliet@", DomainpartByteLimit), ("/foobar", DomainpartByteLimit), ] for jid, exception in tests: with self.assertRaises(InvalidJid) as cm: JID.from_string(jid) self.assertIsInstance(cm.exception.__cause__, exception) def test_invalid_precis_jids(self): os.environ["NBXMPP_ENFORCE_PRECIS"] = "true" tests = [ ("henry\U00002163@example.com", LocalpartNotAllowedChar), ("\U0000265A@example.com", LocalpartNotAllowedChar), ] for jid, exception in tests: with self.assertRaises(InvalidJid) as cm: JID.from_string(jid) self.assertIsInstance(cm.exception.__cause__, exception) del os.environ["NBXMPP_ENFORCE_PRECIS"] def test_resources(self): valid_resources = [ "res👔", "res🔫", ] for res in valid_resources: validate_resourcepart(res) invalid_resources = [ ("res😀️", ResourcepartNotAllowedChar), ("res🤖️", ResourcepartNotAllowedChar), ] for res, exception in invalid_resources: with self.assertRaises(exception): validate_resourcepart(res) 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): # noqa: B017 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): # noqa: B017 JID.from_user_input(user_input) def test_jid_to_iri(self): tests = [ ( "nasty!#$%()*+,-.;=?[\\]^_`{|}~node@example.com", "xmpp:nasty!%23$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com", ), ( "node@example.com/repulsive !#\"$%&'()*+,-./:;<=>?@[\\]^_`{|}~resource", "xmpp:node@example.com/repulsive%20!%23%22$%25&'()*+,-.%2F:;%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource", ), ("jiři@čechy.example/v Praze", "xmpp:jiři@čechy.example/v%20Praze"), ] for jid, iri in tests: jid = JID.from_string(jid) self.assertEqual(jid.to_iri(), iri) jid = JID.from_string("example-node@example.com") iri = jid.to_iri(("message", [("subject", "Hello World")]), "frag") self.assertEqual( iri, "xmpp:example-node@example.com?message;subject=Hello%20World#frag" ) iri = jid.to_iri("message") self.assertEqual(iri, "xmpp:example-node@example.com?message") iri = jid.to_iri(fragment="onlyfragment") self.assertEqual(iri, "xmpp:example-node@example.com#onlyfragment") jid = JID.from_user_input('call me "ishmael"@example.com') self.assertEqual( jid.to_iri(), "xmpp:call%5C20me%5C20%5C22ishmael%5C22@example.com" ) def test_iri_to_jid(self): tests = [ ( "nasty!#$%()*+,-.;=?[\\]^_`{|}~node@example.com", "xmpp:nasty!%23$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com", ), ( "node@example.com/repulsive !#\"$%&'()*+,-./:;<=>?@[\\]^_`{|}~resource", "xmpp:node@example.com/repulsive%20!%23%22$%25&'()*+,-.%2F:;%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource", ), ("jiři@čechy.example/v Praze", "xmpp:jiři@čechy.example/v%20Praze"), ] for jid_string, iri_string in tests: parsed_jid = JID.from_iri(iri_string) self.assertEqual(str(parsed_jid), jid_string) raise_tests = [ "xmpp://node@example.com", "xmpp://node@example.com/test", "xmpp:/auth@example.com/node@example.com", "xmpp:/node@example.com/test", ] for iri_string in raise_tests: with self.assertRaises(InvalidJid): JID.from_iri(iri_string) def test_compare_jid(self): jid1 = JID(localpart="test", domain="test.com", resource="test") jid2 = JID(localpart="test", domain="test.com", resource="test") self.assertNotEqual(id(jid1), id(jid2)) self.assertEqual(jid1, jid2) def test_jid_init(self): with self.assertRaises(DomainpartByteLimit): JID(localpart=None, domain="", resource=None) with self.assertRaises(DomainpartByteLimit): JID(localpart=None, domain=None, resource=None) # pyright: ignore with self.assertRaises(TypeError): JID(localpart=None, domain=None) # pyright: ignore with self.assertRaises(TypeError): JID(domain=None) # pyright: ignore with self.assertRaises(TypeError): JID(localpart=None) # pyright: ignore python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_language.py000066400000000000000000000405471524361513300266200ustar00rootroot00000000000000from __future__ import annotations from typing import Any import collections.abc import unittest from nbxmpp.language import basic_filter_languages from nbxmpp.language import LanguageMap from nbxmpp.language import LanguageRange from nbxmpp.language import LanguageTag from nbxmpp.language import lookup_language class TestLanguageTag(unittest.TestCase): def test_init(self): with self.assertRaises(TypeError): LanguageTag("foo") with self.assertRaises(ValueError): LanguageTag(tag="") with self.assertRaises(TypeError): LanguageTag() def test_fromstr_match_str(self): tag = LanguageTag.fromstr("de-Latn-DE-1999") self.assertEqual("de-latn-de-1999", tag.match_str) def test_fromstr_print_str(self): tag = LanguageTag.fromstr("de-Latn-DE-1999") self.assertEqual("de-Latn-DE-1999", tag.print_str) def test___str__(self): tag = LanguageTag.fromstr("zh-Hans") self.assertEqual("zh-Hans", str(tag)) tag = LanguageTag.fromstr("de-Latn-DE-1999") self.assertEqual("de-Latn-DE-1999", str(tag)) def test_compare_case_insensitively(self): tag1 = LanguageTag.fromstr("de-DE") tag2 = LanguageTag.fromstr("de-de") tag3 = LanguageTag.fromstr("fr") self.assertTrue(tag1 == tag2) self.assertFalse(tag1 != tag2) self.assertTrue(tag2 == tag1) self.assertFalse(tag2 != tag1) self.assertTrue(tag1 != tag3) self.assertFalse(tag1 == tag3) self.assertTrue(tag2 != tag3) self.assertFalse(tag2 == tag3) self.assertTrue(tag3 != tag1) self.assertFalse(tag3 == tag1) self.assertTrue(tag3 != tag1) self.assertFalse(tag3 == tag1) def test_order_case_insensitively(self): tag1 = LanguageTag.fromstr("de-DE") tag2 = LanguageTag.fromstr("de-de") tag3 = LanguageTag.fromstr("en-us") tag4 = LanguageTag.fromstr("fr") self.assertLess(tag1, tag3) self.assertLess(tag1, tag4) self.assertLess(tag2, tag3) self.assertLess(tag2, tag4) self.assertLess(tag3, tag4) self.assertGreater(tag4, tag3) self.assertGreater(tag4, tag2) self.assertGreater(tag4, tag1) self.assertGreater(tag3, tag2) self.assertGreater(tag3, tag1) self.assertFalse(tag1 > tag2) self.assertFalse(tag2 > tag1) self.assertFalse(tag1 < tag2) self.assertFalse(tag2 < tag1) def test_hash_case_insensitively(self): tag1 = LanguageTag.fromstr("de-DE") tag2 = LanguageTag.fromstr("de-de") self.assertEqual(hash(tag1), hash(tag2)) def test_not_equal_to_None(self): tag1 = LanguageTag.fromstr("de-DE") self.assertNotEqual(tag1, None) def test_dont_compare_with_None(self): tag1 = LanguageTag.fromstr("de-DE") with self.assertRaises(TypeError): tag1 > None # noqa: B015 with self.assertRaises(TypeError): tag1 < None # noqa: B015 with self.assertRaises(TypeError): tag1 >= None # noqa: B015 with self.assertRaises(TypeError): tag1 <= None # noqa: B015 def test__repr__(self): tag1 = LanguageTag.fromstr("de-DE") tag2 = LanguageTag.fromstr("fr") self.assertEqual("", repr(tag1)) self.assertEqual("", repr(tag2)) def test_immutable(self): tag = LanguageTag.fromstr("foo") with self.assertRaises(AttributeError): tag.foo = "bar" class TestLanguageRange(unittest.TestCase): def test_init(self): with self.assertRaises(TypeError): LanguageRange("foo") with self.assertRaises(ValueError): LanguageRange(tag="") with self.assertRaises(TypeError): LanguageRange() def test_fromstr_match_str(self): tag = LanguageRange.fromstr("de-DE") self.assertEqual("de-de", tag.match_str) def test_fromstr_print_str(self): tag = LanguageRange.fromstr("de-Latn-DE-1999") self.assertEqual("de-Latn-DE-1999", tag.print_str) def test___str__(self): tag = LanguageRange.fromstr("zh-Hans") self.assertEqual("zh-Hans", str(tag)) tag = LanguageRange.fromstr("de-Latn-DE-1999") self.assertEqual("de-Latn-DE-1999", str(tag)) def test_compare_case_insensitively(self): tag1 = LanguageRange.fromstr("de-DE") tag2 = LanguageRange.fromstr("de-de") tag3 = LanguageRange.fromstr("fr") self.assertTrue(tag1 == tag2) self.assertFalse(tag1 != tag2) self.assertTrue(tag2 == tag1) self.assertFalse(tag2 != tag1) self.assertTrue(tag1 != tag3) self.assertFalse(tag1 == tag3) self.assertTrue(tag2 != tag3) self.assertFalse(tag2 == tag3) self.assertTrue(tag3 != tag1) self.assertFalse(tag3 == tag1) self.assertTrue(tag3 != tag1) self.assertFalse(tag3 == tag1) def test_hash_case_insensitively(self): tag1 = LanguageRange.fromstr("de-DE") tag2 = LanguageRange.fromstr("de-de") self.assertEqual(hash(tag1), hash(tag2)) def test_not_equal_to_None(self): r1 = LanguageRange.fromstr("de-DE") self.assertNotEqual(r1, None) def test_wildcard(self): r1 = LanguageRange.fromstr("*") r2 = LanguageRange.fromstr("*") self.assertIs(r1, r2) def test_strip_rightmost(self): r = LanguageRange.fromstr("de-Latn-DE-x-foo") self.assertEqual(LanguageRange.fromstr("de-Latn-DE"), r.strip_rightmost()) self.assertEqual( LanguageRange.fromstr("de-Latn"), r.strip_rightmost().strip_rightmost() ) self.assertEqual( LanguageRange.fromstr("de"), r.strip_rightmost().strip_rightmost().strip_rightmost(), ) with self.assertRaises(ValueError): r.strip_rightmost().strip_rightmost().strip_rightmost().strip_rightmost() def test_immutable(self): r = LanguageRange.fromstr("foo") with self.assertRaises(AttributeError): r.foo = "bar" class TestBasicFilterLanguages(unittest.TestCase): def setUp(self): self.languages = [ LanguageTag.fromstr("de-Latn-DE-1999"), LanguageTag.fromstr("de-DE"), LanguageTag.fromstr("de-Latn"), LanguageTag.fromstr("fr-CH"), LanguageTag.fromstr("it"), ] def test_filter(self): self.assertSequenceEqual( [ self.languages[0], self.languages[1], self.languages[2], ], list( basic_filter_languages( self.languages, list( map( LanguageRange.fromstr, [ "de", ], ) ), ) ), ) self.assertSequenceEqual( [ self.languages[1], ], list( basic_filter_languages( self.languages, list( map( LanguageRange.fromstr, [ "de-DE", ], ) ), ) ), ) self.assertSequenceEqual( [ self.languages[0], self.languages[2], ], list( basic_filter_languages( self.languages, list( map( LanguageRange.fromstr, [ "de-Latn", ], ) ), ) ), ) def test_filter_no_dupes_and_ordered(self): self.assertSequenceEqual( [ self.languages[0], self.languages[2], self.languages[1], ], list( basic_filter_languages( self.languages, list( map( LanguageRange.fromstr, [ "de-Latn", "de", ], ) ), ) ), ) def test_filter_wildcard(self): self.assertSequenceEqual( self.languages, list( basic_filter_languages( self.languages, list( map( LanguageRange.fromstr, [ "fr", "*", ], ) ), ) ), ) class TestLookupLanguage(unittest.TestCase): def setUp(self): self.languages = [ LanguageTag.fromstr("de-Latn-DE-1999"), LanguageTag.fromstr("fr-CH"), LanguageTag.fromstr("it"), ] def test_match_direct(self): self.assertEqual( LanguageTag.fromstr("fr-CH"), lookup_language( self.languages, list(map(LanguageRange.fromstr, ["en", "fr-ch", "de-de"])), ), ) self.assertEqual( LanguageTag.fromstr("it"), lookup_language( self.languages, list( map( LanguageRange.fromstr, [ "it", ], ) ), ), ) def test_decay(self): self.assertEqual( LanguageTag.fromstr("de-Latn-DE-1999"), lookup_language( self.languages, list(map(LanguageRange.fromstr, ["de-de", "en-GB", "en"])), ), ) self.assertEqual( LanguageTag.fromstr("fr-CH"), lookup_language( self.languages, list( map( LanguageRange.fromstr, [ "fr-FR", "de-DE", "fr", ], ) ), ), ) def test_decay_skips_extension_prefixes_properly(self): self.assertEqual( LanguageTag.fromstr("de-DE"), lookup_language( list( map( LanguageTag.fromstr, [ "de-DE", "de-x", ], ) ), list( map( LanguageRange.fromstr, [ "de-x-foobar", ], ) ), ), ) class TestLanguageMap(unittest.TestCase): def test_implements_mapping(self): mapping = LanguageMap() self.assertIsInstance(mapping, collections.abc.MutableMapping) def test_mapping_interface(self): key1 = LanguageTag.fromstr("de-DE") key2 = LanguageTag.fromstr("en-US") key3 = LanguageTag.fromstr("en") mapping = LanguageMap() self.assertFalse(mapping) self.assertEqual(0, len(mapping)) mapping[key1] = 10 self.assertIn(key1, mapping) self.assertEqual(10, mapping[key1]) self.assertSetEqual({key1}, set(mapping)) mapping[key2] = 20 self.assertIn(key2, mapping) self.assertEqual(20, mapping[key2]) self.assertSetEqual({key1, key2}, set(mapping)) key2_prime = LanguageTag.fromstr("en-us") self.assertIn(key2_prime, mapping) self.assertEqual(20, mapping[key2_prime]) self.assertNotIn(key3, mapping) del mapping[key1] self.assertNotIn(key1, mapping) mapping.clear() self.assertNotIn(key2, mapping) def test_lookup(self): key1 = LanguageTag.fromstr("de-DE") key2 = LanguageTag.fromstr("en-US") key3 = LanguageTag.fromstr("en") mapping = LanguageMap() mapping[key1] = 10 mapping[key2] = 20 mapping[key3] = 30 self.assertEqual(30, mapping.lookup([LanguageRange.fromstr("en-GB")])) def test_values(self): key1 = LanguageTag.fromstr("de-DE") key2 = LanguageTag.fromstr("en-US") key3 = LanguageTag.fromstr("en") mapping = LanguageMap() mapping[key1] = 10 mapping[key2] = 20 mapping[key3] = 30 self.assertSetEqual({10, 20, 30}, set(mapping.values())) def test_keys(self): key1 = LanguageTag.fromstr("de-DE") key2 = LanguageTag.fromstr("en-US") key3 = LanguageTag.fromstr("en") mapping = LanguageMap() mapping[key1] = 10 mapping[key2] = 20 mapping[key3] = 30 self.assertSetEqual({key1, key2, key3}, set(mapping.keys())) def test_items(self): key1 = LanguageTag.fromstr("de-DE") key2 = LanguageTag.fromstr("en-US") key3 = LanguageTag.fromstr("en") mapping = LanguageMap() mapping[key1] = 10 mapping[key2] = 20 mapping[key3] = 30 self.assertSetEqual( { (key1, 10), (key2, 20), (key3, 30), }, set(mapping.items()), ) def test_equality(self): mapping1 = LanguageMap() mapping1[LanguageTag.fromstr("de-de")] = 10 mapping1[LanguageTag.fromstr("en-US")] = 20 mapping2 = LanguageMap() mapping2[LanguageTag.fromstr("de-DE")] = 10 mapping2[LanguageTag.fromstr("en-US")] = 20 mapping3 = LanguageMap() mapping3[LanguageTag.fromstr("de-DE")] = 10 mapping3[LanguageTag.fromstr("en-GB")] = 20 self.assertEqual(mapping1, mapping2) self.assertFalse(mapping1 != mapping2) self.assertNotEqual(mapping1, mapping3) self.assertFalse(mapping1 == mapping3) self.assertNotEqual(mapping2, mapping3) self.assertFalse(mapping2 == mapping3) def test_setdefault(self): l: list[Any] = [] mapping = LanguageMap() result = mapping.setdefault(LanguageTag.fromstr("de-de"), l) self.assertIs(result, l) result = mapping.setdefault(LanguageTag.fromstr("de-de"), []) self.assertIs(result, l) def test_lookup_returns_None_key_if_nothing_matches(self): mapping = LanguageMap() mapping[None] = "foobar" mapping[LanguageTag.fromstr("de")] = "Test" mapping[LanguageTag.fromstr("en")] = "test" self.assertEqual("foobar", mapping.lookup([LanguageRange.fromstr("it")])) def test_any_returns_only_key(self): m = LanguageMap() m[None] = "fnord" self.assertEqual(m.any(), "fnord") m = LanguageMap() m[LanguageTag.fromstr("de")] = "Test" self.assertEqual(m.any(), "Test") def test_any_raises_ValueError_on_empty_map(self): m = LanguageMap() with self.assertRaises(ValueError): m.any() def test_any_prefers_None(self): m = LanguageMap() m[LanguageTag.fromstr("de")] = "Test" m[None] = "fnord" self.assertEqual(m.any(), "fnord") m = LanguageMap() m[None] = "fnord" m[LanguageTag.fromstr("de")] = "Test" self.assertEqual(m.any(), "fnord") def test_any_returns_same_key_for_same_keyset(self): m = LanguageMap() m[LanguageTag.fromstr("de")] = "Test" m[LanguageTag.fromstr("fr")] = "fnord" self.assertEqual(m.any(), "Test") m = LanguageMap() m[LanguageTag.fromstr("fr")] = "fnord" m[LanguageTag.fromstr("de")] = "Test" self.assertEqual(m.any(), "Test") python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_location.py000066400000000000000000000076671524361513300266530ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.namespaces import Namespace from nbxmpp.structs import LocationData from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler 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 = """ 20 1609 10 Central Park 12.33 The Empire State Building United States US Some datum Bill's house 290.8882087 102 39.75 New York City -104.99 10118 New York Observatory 52.69 350 Fifth Avenue / 34th and Broadway Northwest corner of the lobby 2004-02-19T21:12Z -07:00 http://www.nyc.com/ """ self.dispatcher.register_handler( StanzaHandler( name="message", callback=_on_message, ns=Namespace.PUBSUB_EVENT ) ) self.dispatcher.process_data(event) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_moderation.py000066400000000000000000000030011524361513300271560ustar00rootroot00000000000000import datetime from test.lib.util import StanzaHandlerTest from nbxmpp import Message from nbxmpp.modules.muc.moderation import Moderation from nbxmpp.protocol import JID from nbxmpp.structs import MessageProperties class TestModeration(StanzaHandlerTest): def test_parse_moderation_message(self): xml = """ Some Reason """ props = MessageProperties("mockjid") message = Message(node=xml) Moderation._process_moderation_1_message(None, None, message, props) assert props.moderation is not None self.assertEqual(props.moderation.stanza_id, "stanza-id-1") self.assertEqual( props.moderation.by, JID.from_string("room@muc.example.com/macbeth") ) self.assertEqual(props.moderation.reason, "Some Reason") self.assertIsInstance(props.moderation.stamp, datetime.datetime) self.assertFalse(props.moderation.is_tombstone) self.assertEqual( props.moderation.occupant_id, "dd72603deec90a38ba552f7c68cbcc61bca202cd" ) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_mood.py000066400000000000000000000032751524361513300257700ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.namespaces import Namespace from nbxmpp.structs import MoodData from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_node.py000066400000000000000000000030531524361513300257510ustar00rootroot00000000000000import unittest from nbxmpp.simplexml import Node class TestNode(unittest.TestCase): def test_topretty(self): string = """ bb055be7076edf87c9f89e4e0b829f0624aa1cef 127 """ node = Node(node=string) self.assertEqual(node.topretty(), string) string = """ abc fgh """ node = Node(node=string) self.assertEqual(node.topretty(), string) string = """ abc fgh """ node = Node(node=string) self.assertEqual(node.topretty(), string) string = """ abc 123 fgh """ node = Node(node=string) self.assertEqual(node.topretty(), string) string = """ """ node = Node(node=string) self.assertEqual(node.topretty(), string) if __name__ == "__main__": unittest.main() python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_ogp.py000066400000000000000000000134261524361513300256160ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.modules.ogp import OpenGraph from nbxmpp.protocol import Message from nbxmpp.structs import MessageProperties from nbxmpp.structs import OpenGraphData class MockOpenGraph(OpenGraph): def __init__(self) -> None: pass class OpenGraphTest(StanzaHandlerTest): def test_parse_ogp(self) -> None: msg = Message(node=_DATA) self._parse_and_assert_values(msg) def test_parse_wikipedia(self) -> None: msg = Message( node=""" Wikipédia, l'encyclopédie libre https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:Accueil_principal https://fr.wikipedia.org """ ) props = self._parse(msg) ogp = props["https://fr.wikipedia.org"] self.assertEqual(ogp.title, "Wikipédia, l'encyclopédie libre") self.assertEqual( ogp.url, "https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:Accueil_principal" ) def test_parse_youtube(self) -> None: msg = Message( node=""" Pierre Lapointe - Je déteste ma vie (Paris tristesse) - YouTube Paris tristesse en vente maintenant: http://smarturl.it/paristristesseRéalisation | montage : Pascal Grandmaison et Philippe CraigDirection photo: Pascal Gra... https://www.youtube.com/watch?v=45V5qdjhbGI&t=1 YouTube https://www.youtube.com/watch?v=45V5qdjhbGI&t=1 """ ) props = self._parse(msg) ogp = props["https://www.youtube.com/watch?v=45V5qdjhbGI&t=1"] self.assertEqual( ogp.title, "Pierre Lapointe - Je déteste ma vie (Paris tristesse) - YouTube" ) self.assertEqual( ogp.description, "Paris tristesse en vente maintenant: http://smarturl.it/paristristesseRéalisation | montage : Pascal Grandmaison et Philippe CraigDirection photo: Pascal Gra...", ) self.assertEqual(ogp.site_name, "YouTube") def test_set_ogp(self) -> None: msg1 = Message(node=_DATA) msg2 = Message(to=msg1.getTo(), body=msg1.getBody()) msg2.addOpenGraph( "https://the.link.example.com/what-was-linked-to", OpenGraphData( title="Page Title", url="Canonical URL", image="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDoAAAH", type="website", site_name="Some Website", description="Page Description", ), ) self._parse_and_assert_values(msg2) def _parse(self, msg: Message) -> dict[str, OpenGraphData]: properties = MessageProperties("whatever@whatever.com") parser = MockOpenGraph() parser._process_message_opengraph(None, msg, properties) self.assertEqual(len(properties.open_graph), 1) return properties.open_graph def _parse_and_assert_values(self, msg: Message) -> None: props = self._parse(msg) ogp = props["https://the.link.example.com/what-was-linked-to"] self.assertEqual(ogp.title, "Page Title") self.assertEqual(ogp.url, "Canonical URL") self.assertEqual( ogp.image, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDoAAAH" ) self.assertEqual(ogp.type, "website") self.assertEqual(ogp.site_name, "Some Website") self.assertEqual(ogp.description, "Page Description") _DATA = """ I wanted to mention https://the.link.example.com/what-was-linked-to Page Title Page Description Canonical URL data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDoAAAH website Some Website """ python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_omemo.py000066400000000000000000000027231524361513300261430ustar00rootroot00000000000000import unittest from nbxmpp.modules.omemo import _parse_bundle from nbxmpp.modules.omemo import _parse_devicelist from nbxmpp.simplexml import Node class OMEMOTest(unittest.TestCase): def test_parsing(self): devicelist = """ """ devices = _parse_devicelist(Node(node=devicelist)) self.assertEqual(devices, [912561474, 532656838]) bundle = """ dGVzdHBheWxvYWQ= dGVzdHBheWxvYWQ= dGVzdHBheWxvYWQ= dGVzdHBheWxvYWQ= dGVzdHBheWxvYWQ= dGVzdHBheWxvYWQ= dGVzdHBheWxvYWQ= """ omemo_bundle = _parse_bundle(Node(node=bundle), 1) self.assertEqual(len(omemo_bundle.otpks), 3) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_pubsub.py000066400000000000000000000062361524361513300263320ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.namespaces import Namespace from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler 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-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_reactions.py000066400000000000000000000106721524361513300270200ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.modules.reactions import Reactions from nbxmpp.protocol import Message from nbxmpp.structs import Reactions as ReactionStruct class MockLog: @staticmethod def warning(_): pass class MockModule: _log = MockLog @staticmethod def is_emoji(s): return Reactions.is_emoji(s) class ReactionsTest(StanzaHandlerTest): def test_reaction_parsing(self): class P: reactions: ReactionStruct xml = """ 👋 🐢 """ msg = Message(node=xml) Reactions._process_message_reaction(MockModule, self, msg, P) self.assertEqual(P.reactions.id, "744f6e18-a57a-11e9-a656-4889e7820c76") self.assertEqual(P.reactions.emojis, {"👋", "🐢"}) def test_no_reactions(self): class P: reactions: ReactionStruct = None xml = """ """ msg = Message(node=xml) Reactions._process_message_reaction(MockModule, self, msg, P) self.assertIsNone(P.reactions) def test_empty_reactions(self): class P: reactions: ReactionStruct xml = """ """ msg = Message(node=xml) Reactions._process_message_reaction(MockModule, self, msg, P) self.assertEqual(len(P.reactions.emojis), 0) def test_invalid_reactions_no_id(self): class P: reactions: ReactionStruct xml = """ 👋 🐢 """ msg = Message(node=xml) Reactions._process_message_reaction(MockModule, self, msg, P) self.assertFalse(hasattr(P, "reactions")) def test_invalid_reactions_empty_id(self): class P: reactions: ReactionStruct xml = """ 👋 🐢 """ msg = Message(node=xml) Reactions._process_message_reaction(MockModule, self, msg, P) self.assertFalse(hasattr(P, "reactions")) def test_invalid_reactions_empty_emoji(self): class P: reactions: ReactionStruct xml = """ 🐢 """ msg = Message(node=xml) Reactions._process_message_reaction(MockModule, self, msg, P) self.assertEqual(P.reactions.emojis, {"🐢"}) def test_set_reactions(self): x = Message() x.setReactions("id", "🐢") self.assertEqual(x.getReactions(), ("id", {"🐢"})) x = Message() x.setReactions("id", "🐢👋") self.assertEqual(x.getReactions(), ("id", {"🐢", "👋"})) x = Message() x.setReactions("id", "") self.assertEqual(x.getReactions(), ("id", set())) x = Message() self.assertIsNone(x.getReactions()) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_restricted_reactions.py000066400000000000000000000075021524361513300312460ustar00rootroot00000000000000import unittest from nbxmpp.modules.discovery import parse_disco_info from nbxmpp.protocol import Iq class RestrictedReactions(unittest.TestCase): def test_form_present(self): node = """ urn:xmpp:dataforms:softwareinfo ipv4 ipv6 Mac 10.5.1 Psi 0.11 urn:xmpp:reactions:0:restrictions 1 💘 ❤️ 💜 """ info = parse_disco_info(Iq(node=node)) self.assertEqual(info.reactions_per_user, 1) self.assertEqual(info.reactions_allowlist, ["💘", "❤️", "💜"]) def test_form_absent(self): node = """ urn:xmpp:dataforms:softwareinfo ipv4 ipv6 Mac 10.5.1 Psi 0.11 """ info = parse_disco_info(Iq(node=node)) self.assertIsNone(info.reactions_per_user) self.assertIsNone(info.reactions_allowlist) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_retraction.py000066400000000000000000000070501524361513300271770ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from test.lib.util import utc_now from nbxmpp import Message from nbxmpp import Namespace from nbxmpp.modules.retraction import Retraction from nbxmpp.protocol import JID from nbxmpp.structs import MAMData from nbxmpp.structs import MessageProperties class TestRetraction(StanzaHandlerTest): def test_parse_retract_direct_message(self): # language=XML xml = """ This person attempted to retract a previous message """ timestamp = utc_now() props = MessageProperties( JID.from_string("mockjid@jid.com"), timestamp=timestamp.timestamp() ) message = Message(node=xml) Retraction._process_message_retraction(None, None, message, props) assert props.retraction is not None self.assertEqual(props.retraction.id, "origin-id-1") self.assertFalse(props.retraction.is_tombstone) self.assertEqual(props.retraction.timestamp, timestamp) def test_parse_retract_groupchat_message(self): # language=XML xml = """ This person attempted to retract a previous message """ timestamp = utc_now() props = MessageProperties( JID.from_string("mockjid@jid.com"), timestamp=timestamp.timestamp() ) message = Message(node=xml) Retraction._process_message_retraction(None, None, message, props) assert props.retraction is not None self.assertEqual(props.retraction.id, "stanza-id-1") self.assertFalse(props.retraction.is_tombstone) self.assertEqual(props.retraction.timestamp, timestamp) def test_parse_retract_tombstone_message(self): # language=XML xml = """ """ timestamp = utc_now() mam = MAMData( id="1", query_id="query1", archive=JID.from_string("archive@jid.com"), namespace="mam:1", timestamp=timestamp.timestamp(), ) props = MessageProperties(JID.from_string("mockjid@jid.com"), mam=mam) message = Message(node=xml) Retraction._process_message_retraction(None, None, message, props) assert props.retraction is not None self.assertEqual(props.retraction.id, "ref-to-retract-message-id") self.assertTrue(props.retraction.is_tombstone) self.assertEqual(props.retraction.timestamp, timestamp) def test_set_retraction(self): msg = Message() msg.setRetracted("some-complex-xmpp-message-id") self.assertEqual( msg.getTagAttr("retract", "id", Namespace.MESSAGE_RETRACT_1), "some-complex-xmpp-message-id", ) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_sasl_scram.py000066400000000000000000000023231524361513300271520ustar00rootroot00000000000000import unittest from unittest.mock import Mock from nbxmpp.sasl 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.maxDiff = None self._username = "user" self._password = "pencil" # noqa: S105 self._mechanism = SCRAM_SHA_1(self._username, self._password, None) self._mechanism._client_nonce = "fyko+d2lbbFgONRv9qkxdawL" def test_auth(self): initial = b64encode("n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL") data = self._mechanism.get_initiate_data() self.assertEqual(data, initial) challenge = b64encode( "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096" ) data = self._mechanism.get_response_data(challenge) response = b64encode( "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=" ) self.assertEqual(data, response) success = b64encode("v=rmF9pqV8S7suAoZWja4dJRkFsKQ=") self._mechanism.validate_success_data(success) if __name__ == "__main__": unittest.main() python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_stringprep.py000066400000000000000000000154411524361513300272250ustar00rootroot00000000000000import unittest from nbxmpp.stringprep import check_bidi from nbxmpp.stringprep import nameprep from nbxmpp.stringprep import nodeprep from nbxmpp.stringprep import resourceprep from nbxmpp.stringprep import saslprep class TestBidi(unittest.TestCase): def test_empty_string(self): check_bidi("") def test_L_RAL_violation(self): with self.assertRaises(ValueError): check_bidi("\u05be\u0041") class TestNodeprep(unittest.TestCase): def test_map_to_nothing(self): self.assertEqual( "ix", nodeprep("I\u00ADX"), "Nodeprep requirement: map SOFT HYPHEN to nothing", ) def test_case_fold(self): self.assertEqual( "ssa", nodeprep("ßA"), "Nodeprep requirement: map ß to ss, A to a" ) def test_nfkc(self): self.assertEqual("a", nodeprep("\u00AA"), "Nodeprep requirement: NFKC") self.assertEqual("ix", nodeprep("\u2168"), "Nodeprep requirement: NFKC") def test_prohibited_character(self): with self.assertRaisesRegex( ValueError, r"U\+0007", msg="Nodeprep requirement: prohibited character (C.2.1)", ): nodeprep("\u0007") with self.assertRaisesRegex( ValueError, r"U\+200e", msg="Nodeprep requirement: prohibited character (C.8)", ): nodeprep("\u200E") with self.assertRaisesRegex( ValueError, r"U\+003e", msg="Nodeprep requirement: prohibited character (custom)", ): nodeprep(">") def test_unassigned(self): with self.assertRaises(ValueError, msg="Nodeprep requirement: unassigned"): nodeprep("\u0221", allow_unassigned=False) with self.assertRaises(ValueError, msg="enforce no unassigned by default"): nodeprep("\u0221") self.assertEqual("\u0221", nodeprep("\u0221", allow_unassigned=True)) class TestNameprep(unittest.TestCase): def test_map_to_nothing(self): self.assertEqual( "ix", nameprep("I\u00ADX"), "Nameprep requirement: map SOFT HYPHEN to nothing", ) def test_case_fold(self): self.assertEqual( "ssa", nameprep("ßA"), "Nameprep requirement: map ß to ss, A to a" ) def test_nfkc(self): self.assertEqual("a", nodeprep("\u00AA"), "Nameprep requirement: NFKC") self.assertEqual("ix", nodeprep("\u2168"), "Nameprep requirement: NFKC") def test_prohibited_character(self): with self.assertRaisesRegex( ValueError, r"U\+06dd", msg="Nameprep requirement: prohibited character (C.2.2)", ): nameprep("\u06DD") with self.assertRaisesRegex( ValueError, r"U\+e000", msg="Nameprep requirement: prohibited character (C.3)", ): nameprep("\uE000") with self.assertRaisesRegex( ValueError, r"U\+1fffe", msg="Nameprep requirement: prohibited character (C.4)", ): nameprep("\U0001FFFE") with self.assertRaisesRegex( ValueError, r"U\+d800", msg="Nameprep requirement: prohibited character (C.5)", ): nameprep("\uD800") with self.assertRaisesRegex( ValueError, r"U\+fff9", msg="Nameprep requirement: prohibited character (C.6)", ): nameprep("\uFFF9") with self.assertRaisesRegex( ValueError, r"U\+2ff0", msg="Nameprep requirement: prohibited character (C.7)", ): nameprep("\u2FF0") with self.assertRaisesRegex( ValueError, r"U\+e0001", msg="Nameprep requirement: prohibited character (C.9)", ): nameprep("\U000E0001") def test_unassigned(self): with self.assertRaises(ValueError, msg="Nameprep requirement: unassigned"): nameprep("\u0221", allow_unassigned=False) with self.assertRaises(ValueError, msg="enforce no unassigned by default"): nameprep("\u0221") self.assertEqual("\u0221", nameprep("\u0221", allow_unassigned=True)) class TestResourceprep(unittest.TestCase): def test_map_to_nothing(self): self.assertEqual( "IX", resourceprep("I\u00ADX"), "Resourceprep requirement: map SOFT HYPHEN to nothing", ) def test_nfkc(self): self.assertEqual("a", resourceprep("\u00AA"), "Resourceprep requirement: NFKC") self.assertEqual("IX", resourceprep("\u2168"), "Resourceprep requirement: NFKC") def test_prohibited_character(self): with self.assertRaisesRegex( ValueError, r"U\+0007", msg="Resourceprep requirement: " "prohibited character (C.2.1)", ): resourceprep("\u0007") with self.assertRaisesRegex( ValueError, r"U\+200e", msg="Resourceprep requirement: " "prohibited character (C.8)", ): resourceprep("\u200E") def test_unassigned(self): with self.assertRaises(ValueError, msg="Resourceprep requirement: unassigned"): resourceprep("\u0221", allow_unassigned=False) with self.assertRaises(ValueError, msg="enforce no unassigned by default"): resourceprep("\u0221") self.assertEqual("\u0221", resourceprep("\u0221", allow_unassigned=True)) class TestSASLprep(unittest.TestCase): def test_map_to_nothing(self): self.assertEqual( "IX", saslprep("I\u00ADX"), "SASLprep requirement: map SOFT HYPHEN to nothing", ) def test_nfkc(self): self.assertEqual("a", saslprep("\u00AA"), "SASLprep requirement: NFKC") self.assertEqual("IX", saslprep("\u2168"), "SASLprep requirement: NFKC") def test_case_fold(self): self.assertNotEqual( "user", saslprep("USER"), "SASLprep requirement: No CaseFold" ) def test_prohibited_character(self): with self.assertRaisesRegex( ValueError, r"U\+0007", msg="SASLprep requirement: " "prohibited character (C.2.1)", ): saslprep("\u0007") def test_bidi_check(self): with self.assertRaises(ValueError, msg="SASLprep requirement: bidi check"): saslprep("\u0627\u0031") def test_unassigned(self): with self.assertRaises(ValueError, msg="SASLprep requirement: unassigned"): saslprep("\u0221", allow_unassigned=False) with self.assertRaises(ValueError, msg="enforce no unassigned by default"): saslprep("\u0221") self.assertEqual("\u0221", saslprep("\u0221", allow_unassigned=True)) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_tune.py000066400000000000000000000042741524361513300260050ustar00rootroot00000000000000from test.lib.util import StanzaHandlerTest from nbxmpp.namespaces import Namespace from nbxmpp.structs import PubSubEventData from nbxmpp.structs import StanzaHandler from nbxmpp.structs import TuneData 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 = """ Yes 686 8 Yessongs Heart of the Sunrise 3 https://www.artist.com """ self.dispatcher.register_handler( StanzaHandler( name="message", callback=_on_message, ns=Namespace.PUBSUB_EVENT ) ) self.dispatcher.process_data(event) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_vcard4.py000066400000000000000000000145011524361513300262070ustar00rootroot00000000000000import unittest from nbxmpp.modules.vcard4 import LanguageParameter from nbxmpp.modules.vcard4 import VCard from nbxmpp.simplexml import Node class TestVCard4(unittest.TestCase): def test_vard4_parsing(self): vcard_node = Node( node=""" Peter Saint-Andre fr Saint-Andre Peter stpeter psa https://stpeter.im/images/stpeter_oscon.jpg 1966-08-06 workvoice 1 Suite 600 1899 Wynkoop Street Denver CO 80202 USA home Parker CO 80138 USA workvoice 1 tel:+1-303-308-3282 workfax tel:+1-303-308-3219 cellvoicetext tel:+1-720-256-6756 homevoice tel:+1-303-555-1212 geo:39.59,-105.01 <text>Executive Director</text> Patron Saint work XMPP Standards Foundation https://stpeter.im/ More information about me is located on my personal website: https://stpeter.im/ M 1 en work psaintan@cisco.com home stpeter@jabber.org work xmpp:psaintan@cisco.com home xmpp:stpeter@jabber.org https://stpeter.im/stpeter.asc he/him unsupported """ ) vcard = VCard.from_node(vcard_node) props = vcard.get_properties() email_props = list(filter(lambda p: p.name == "email", props)) self.assertEqual(len(email_props), 2) nickname_props = list(filter(lambda p: p.name == "nickname", props)) self.assertEqual(len(nickname_props), 2) n_props = list(filter(lambda p: p.name == "n", props)) self.assertEqual(len(n_props), 1) n_prop = n_props[0] lang_param = n_prop.parameters.get_parameter("language") assert isinstance(lang_param, LanguageParameter) self.assertEqual(lang_param.value, "fr") pronouns_props = list(filter(lambda p: p.name == "pronouns", props)) self.assertEqual(len(pronouns_props), 1) self.assertEqual(pronouns_props[0].value, "he/him") # Preserve unsupported elements node = vcard.to_node() self.assertEqual(node.getTagData("unsupported-element"), "unsupported") def test_vcard4_pronouns(self): vcard_node = Node( node=""" Cartman fr 1 il/lui he/him """ ) vcard = VCard.from_node(vcard_node) props = vcard.get_properties() pronouns_props = list(filter(lambda p: p.name == "pronouns", props)) self.assertEqual(len(pronouns_props), 2) self.assertEqual( pronouns_props[0].parameters.get_parameter("language").value, "fr" ) python-nbxmpp-7.4.0-51b4aee436b12a280b7103ffb0378382912b0533/test/unit/test_xml_vulnerability.py000066400000000000000000000045251524361513300306020ustar00rootroot00000000000000import 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()