9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz

This commit is contained in:
emdee 2022-10-19 18:09:31 +00:00
parent a5b3822651
commit 4c1b226bff
1045 changed files with 753037 additions and 18 deletions

674
COPYING Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <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 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 <http://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

16
LICENSE
View File

@ -1,16 +0,0 @@
Copyright (c) year copyright holder. All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1.
Redistribution of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2.
Redistribution in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3.
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
YOU ACKNOWLEDGE THAT THIS SOFTWARE IS NOT DESIGNED, LICENSED OR INTENDED FOR USE IN THE DESIGN, CONSTRUCTION, OPERATION OR MAINTENANCE OF ANY MILITARY FACILITY.

5
MANIFEST.in Normal file
View File

@ -0,0 +1,5 @@
include COPYING
recursive-include po *.po
recursive-include data *.1 *.in
recursive-include test *.py
recursive-exclude * *.pyc

12
PKG-INFO Normal file
View File

@ -0,0 +1,12 @@
Metadata-Version: 1.2
Name: gajim
Version: 1.3.3
Summary: A GTK XMPP client
Home-page: https://gajim.org
Author: Philipp Hoerist, Yann Leboulanger
Author-email: gajim-devel@gajim.org
License: GPL v3
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.7

101
README.md
View File

@ -1,3 +1,100 @@
# gajim3
# Welcome to Gajim
gajim 1.3.3 hard forked
### Runtime Requirements
- python3.7 or higher
- python3-gi
- python3-gi-cairo
- gir1.2-gtk-3.0 (>=3.22)
- python3-nbxmpp (>=2.0.4)
- python3-openssl (>=16.2)
- python3-css-parser
- python3-keyring
- python3-precis-i18n
- python3-packaging
- python3-setuptools
- gir1.2-soup-2.4
- GLib (>=2.60.0)
### Optional Runtime Requirements
- python3-pil (pillow) for support of webp avatars
- gir1.2-avahi-0.6 for zeroconf on Linux or [pybonjour](https://dev.gajim.org/lovetox/pybonjour-python3) on Windows/macOS
- gir1.2-gspell-1 and hunspell-LANG where lang is your locale eg. en, fr etc
- gir1.2-secret-1 for GNOME Keyring or KDE support as password storage
- D-Bus running to have gajim-remote working
- gir1.2-farstream-0.2, gir1.2-gstreamer-1.0 gir1.2-gst-plugins-base-1.0, gstreamer1.0-plugins-ugly, gstreamer1.0-libav, and gstreamer1.0-gtk3 for audio and video calls
- gir1.2-gupnpigd-1.0 for better NAT traversing
- gir1.2-networkmanager-1.0 for network lose detection
- gir1.2-geoclue-2.0 for sharing your location
- gir1.2-gsound-1.0 for sound on Linux
### Compile-time Requirements
- python3-setuptools
- gettext
### Installation Procedure
#### Packages
- [Arch Linux](https://www.archlinux.org/packages/community/any/gajim/)
- [Debian](https://packages.debian.org/stable/gajim)
- [Fedora](https://apps.fedoraproject.org/packages/gajim)
- [Ubuntu](https://packages.ubuntu.com/gajim)
- [FreeBSD](https://www.freshports.org/net-im/gajim/)
#### Flatpak
see [README](./flatpak/README.md)
#### Snapshots
- [Daily Linux](https://www.gajim.org/downloads/snap/)
- [Daily Windows](https://gajim.org/downloads/snap/win)
#### Linux
pip install .
#### Mac
see [Wiki](https://dev.gajim.org/gajim/gajim/wikis/help/gajimmacosx#python3brew)
#### Developing
For developing you don't have to install Gajim.
After installing all dependencies execute
./launch.py
#### Windows
see [README](./win/README.md)
### Miscellaneous
#### Debugging
Execute gajim with `--verbose`
#### Links
- [FAQ](https://dev.gajim.org/gajim/gajim/wikis/help/gajimfaq)
- [Wiki](https://dev.gajim.org/gajim/gajim/wikis/home)
That is all, **enjoy**!
(C) 2003-2021
The Gajim Team
[https://gajim.org](https://gajim.org)
We use original art and parts of sounds and other art from Psi, Gossip, Gnomebaker, Gaim
and some icons from various gnome-icons (mostly Dropline Etiquette) we found at art.gnome.org.
If you think we're violating a license please inform us. Thank you.

View File

@ -0,0 +1,86 @@
.Dd January 21, 2018
.Dt GAJIM-HISTORY-MANAGER 1 URM
.Os UNIX
.Sh NAME
.Nm gajim-history-manager
.Nd a tool to manage
.Xr gajim 1
logs
.Sh SYNOPSIS
.Nm
.Fl h
.Nm
.Op Fl c Ar directory
.Sh DESCRIPTION
.Nm
is a tool to manage
.Po do some cleanup Pc log file of
.Xr gajim 1 .
Use this program to delete or export logs.
For more information on database logs see <https://trac.gajim.org/wiki/LogsDatabase>.
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl h Fl Fl help
Show help options
.It Fl c Fl Fl config-path Em directory
Where to look for logs file
.El
.Sh FILES
.Bl -tag -width Ds
.It $XDG_DATA_HOME/gajim/logs.db
The history database log file used when
.Op Fl c
is not specified.
.El
.Sh AUTHORS
.An -nosplit
.Nm
is written and maintained by
.An Yann Leboulanger ,
and
.An Denis Fomin ,
with contributions and patches merged from many individuals around the world.
See files
.Pa AUTHORS
and
.Pa THANKS ,
for a complete list.
.Sh COPYRIGHT
Copyright (C) 2003-2021 Gajim Team
.Pp
.Nm
is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 3 only.
.Pp
.Nm
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.
.Pp
You should have received a copy of the GNU General Public License along with
.Nm .
If not, see <https://www.gnu.org/licenses/>.
.Sh FEEDBACK
You can report bugs or feature requests in our bug tracker at
.Em https://dev.gajim.org/gajim/gajim/issues
or in the
.Em gajim-devel
mailing list; if you want to send us a patch, please do so in our bug tracker.
You can also find us in our chat room.
.Sh WWW
https://www.gajim.org/
.Sh XMPP
You are welcome to join us at gajim@conference.gajim.org
.Sh MAILING LIST
Below are public mailing lists on lists.gajim.org
.Bd -literal -offset indent
https://lists.gajim.org/cgi-bin/listinfo/gajim-devel
https://lists.gajim.org/cgi-bin/listinfo/translators
.Ed
.Pp
More mailing lists at
.Bd -literal -offset indent
https://lists.gajim.org/cgi-bin/listinfo
.Ed
.Sh BUGS
Please submit bugs at https://dev.gajim.org/gajim/gajim/issues
.Sh SEE ALSO
.Xr gajim 1
.Xr gajim-remote 1

119
data/gajim-remote.1 Normal file
View File

@ -0,0 +1,119 @@
.Dd January 21, 2018
.Dt GAJIM-REMOTE 1 URM
.Os UNIX
.Sh NAME
.Nm gajim-remote
.Nd a remote control utility for
.Xr gajim 1
.Sh SYNOPSIS
.Nm
.Ar command
.Sh DESCRIPTION
.Nm
is a script to control and communicate with a running instance of
.Xr gajim 1
by D-Bus.
.Sh OPTIONS
.Bl -tag -width Ds
.It Available commands
.El
.Ss account_info Aq account
Gets detailed info on a account
.Ss change_status Bo status Bc Bo message Bc Bq account
Changes the status of account or accounts
.Ss check_gajim_running
Check if Gajim is running
.Ss contact_info Aq jid
Gets detailed info on a contact
.Ss get_status Bq account
Returns current status (the global one unless account is specified)
.Ss get_status_message Bq account
Returns current status message (the global one unless account is specified)
.Ss get_unread_msgs_number
Returns number of unread messages
.Ss help Bq command
Shows a help on specific command
.Ss list_accounts
Prints a list of registered accounts
.Ss list_contacts Bq account
Prints a list of all contacts in the roster. Each contact appears on a separate line
.Ss remove_contact Ao jid Ac Bq account
Removes contact from roster
.Ss send_chat_message Ao jid Ac Ao message Ac Bo PGP key Bc Bq account
Sends new chat message to a contact in the roster. Both OpenPGP key and account are optional. If you want to set only 'account', without 'OpenPGP key', just set 'OpenPGP key' to ''.
.Ss send_file Ao file Ac Ao jid Ac Bq account
Sends file to a contact
.Ss send_groupchat_message Ao room_jid Ac Ao message Ac Bq account
Sends new message to a groupchat you've joined.
.Ss send_single_message Ao jid subject Ac Ao message Ac Bo PGP key Bc Bq account
Sends new single message to a contact in the roster. Both OpenPGP key and account are optional. If you want to set only 'account', without 'OpenPGP key', just set 'OpenPGP key' to ''.
.Ss send_xml Ao xml Ac Bq account
Sends custom XML
.Ss set_priority Ao priority Ac Bq account
Changes the priority of account or accounts
.Ss show_next_pending_event
Pops up a window with the next pending event
.Ss toggle_ipython
Shows or hides the ipython window
.Ss toggle_roster_appearance
Shows or hides the roster window
.Sh EXAMPLES
Open a URI of group chat gajim
.Pp
.Dl $ gajim-remote handle_uri xmpp:gajim@conference.gajim.org?join
.Pp
Send custom XML
.Pp
.Dl $ gajim-remote send_xml $(cat Pa filename.xml ) Qq your@jabber.id
.Sh AUTHORS
.An -nosplit
.Nm
is written and maintained by
.An Yann Leboulanger ,
and
.An Denis Fomin ,
with contributions and patches merged from many individuals around the world.
See files
.Pa AUTHORS
and
.Pa THANKS ,
for a complete list.
.Sh COPYRIGHT
Copyright (C) 2003-2021 Gajim Team
.Pp
.Nm
is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 3 only.
.Pp
.Nm
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.
.Pp
You should have received a copy of the GNU General Public License along with
.Nm .
If not, see <https://www.gnu.org/licenses/>.
.Sh FEEDBACK
You can report bugs or feature requests in our bug tracker at
.Em https://dev.gajim.org/gajim/gajim/issues
or in the
.Em gajim-devel
mailing list; if you want to send us a patch, please do so in our bug tracker.
You can also find us in our chat room.
.Sh WWW
https://www.gajim.org/
.Sh XMPP
You are welcome to join us at gajim@conference.gajim.org
.Sh MAILING LIST
Below are public mailing lists on lists.gajim.org
.Bd -literal -offset indent
https://lists.gajim.org/cgi-bin/listinfo/gajim-devel
https://lists.gajim.org/cgi-bin/listinfo/translators
.Ed
.Pp
More mailing lists at
.Bd -literal -offset indent
https://lists.gajim.org/cgi-bin/listinfo
.Ed
.Sh BUGS
Please submit bugs at https://dev.gajim.org/gajim/gajim/issues
.Sh SEE ALSO
.Xr gajim 1
.Xr gajim-history-manager 1

138
data/gajim.1 Normal file
View File

@ -0,0 +1,138 @@
.Dd January 21, 2018
.Dt GAJIM 1 URM
.Os UNIX
.Sh NAME
.Nm gajim
.Nd a Jabber/XMPP client
.Sh SYNOPSIS
.Nm
.Fl h
.Nm
.Op Fl q
.Op Fl v
.Op Fl w
.Op Fl l Ar subsystem=level
.Op Fl p Ar name
.Op Fl s
.Op Fl c Ar directory
.Sh DESCRIPTION
.Nm
is a Jabber/XMPP client written in Python and GTK+.
.Nm
works nicely with GNOME, yet it does not require it to run.
.Nm
is designed for novice and advanced users as one, as well for XMPP
server admins.
.Pp
XMPP is the Extensible Messaging and Presence Protocol, a set of open
technologies for instant messaging, presence, multi-party chat, voice
and video calls, collaboration, lightweight middleware, content
syndication, and generalized routing of XML data. For more information
on the XMPP protocol see <https://xmpp.org/about-xmpp/>.
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl h Fl Fl help
Show help options
.It Fl q Fl Fl quiet
Show only critical errors
.It Fl s Fl Fl separate
Separate profiles completely (even history db and plugins)
.It Fl v Fl Fl verbose
Print xml stanzas and other debug information
.It Fl w Fl Fl warnings
Show all GTK warnings with traceback
.It Fl l Fl Fl loglevel Em subsystem=level Bq , Em subsystem=level Bq Em ...
Configure logging system.
Subsystem can be
.Em gajim.interface ,
.Em gajim.c.connection ,
.Em .nbxmpp.client_nb ,
etc.
Level can be
.Em DEBUG ,
.Em INFO ,
.Em WARNING ,
.Em ERROR
or
.Em CRITICAL .
.It Fl p Fl Fl profile Em name
Run
.Nm
using
.Pa config.name
in configuration directory
.It Fl c Fl Fl config-path Em directory
Where to look for configuration files
.El
.Sh FILES
.Bl -tag -width Ds
.It $XDG_CACHE_HOME/gajim/cache.db
Cache database file of transports, caps, roster entry, and roster group.
.It $XDG_CACHE_HOME/gajim/avatars/
Cache directory of avatars.
.It $XDG_CACHE_HOME/gajim/vcards/
Cache directory of vCards (virtual cards).
.It $XDG_CONFIG_HOME/gajim/
The config-path used when
.Op Fl c
is not specified.
.It $XDG_DATA_HOME/gajim/certs/
Directory where certificates are stored.
.It $XDG_DATA_HOME/gajim/logs.db
The history database log file.
For more information on database logs see
<https://dev.gajim.org/gajim/gajim/wikis/development/LogsDatabase>.
.El
.Sh AUTHORS
.An -nosplit
.Nm
is written and maintained by
.An Yann Leboulanger
and
.An Philipp Hörist ,
with contributions and patches merged from many individuals around the world.
See the About Dialog for a complete list.
.Sh COPYRIGHT
Copyright (C) 2003-2021 Gajim Team
.Pp
.Nm
is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 3 only.
.Pp
.Nm
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.
.Pp
You should have received a copy of the GNU General Public License along with
.Nm .
If not, see <https://www.gnu.org/licenses/>.
.Sh FEEDBACK
You can report bugs or feature requests in our bug tracker at
.Em https://dev.gajim.org/gajim/gajim/issues
or in the
.Em gajim-devel
mailing list; if you want to send us a patch, please do so in our bug tracker.
You can also find us in our chat room.
.Sh WWW
https://www.gajim.org/
.Sh XMPP
You are welcome to join us at gajim@conference.gajim.org
.Sh MAILING LIST
Below are public mailing lists on lists.gajim.org
.Bd -literal -offset indent
https://lists.gajim.org/cgi-bin/listinfo/gajim-devel
https://lists.gajim.org/cgi-bin/listinfo/translators
.Ed
.Pp
More mailing lists at
.Bd -literal -offset indent
https://lists.gajim.org/cgi-bin/listinfo
.Ed
.Sh BUGS
Please submit bugs at
.Bd -literal -offset indent
https://dev.gajim.org/gajim/gajim/issues
https://dev.gajim.org/gajim/gajim-plugins
https://dev.gajim.org/gajim/python-nbxmpp
.Ed
.Sh SEE ALSO
.Xr gajim-remote 1
.Xr gajim-history-manager 1

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017-2021 Gajim Team -->
<component type="desktop-application">
<id>org.gajim.Gajim</id>
<metadata_license>CC-BY-SA-3.0</metadata_license>
<project_license>GPL-3.0</project_license>
<name>Gajim</name>
<summary>A fully-featured XMPP chat client</summary>
<description>
<p>Gajim aims to be an easy to use and fully-featured XMPP client.
Just chat with your friends or family, easily share pictures and
thoughts or discuss the news with your groups.
</p>
<p>Chat securely with End-to-End encryption via OMEMO or OpenPGP.</p>
<p>Gajim integrates well with your other devices: simply continue conversations on your mobile device.
</p>
<p>Features:</p>
<ul>
<li>Never miss a message, keep all your chat clients synchronized</li>
<li>Invite friends to group chats or join one</li>
<li>Easily send pictures, videos or other files to friends and groups</li>
<li>Chat securely with End-to-End encryption via OMEMO or OpenPGP</li>
<li>Use your favorite emoticons, set your own profile picture</li>
<li>Keep and manage all your chat history</li>
<li>Organize your chats with tabs</li>
<li>Automatic spell-checking for your messages</li>
<li>Connect to other Messengers via Transports (Facebook, IRC, ...)</li>
<li>Lookup things on Wikipedia, dictionaries or other search engines directly from the chat window</li>
<li>Set your activity, tune, and mood to show your friends how you are feeling</li>
<li>Support for multiple accounts</li>
<li>Group multiple contacts from one friend to a single Meta-Contact</li>
<li>XML console to see what's happening on the protocol layer</li>
<li>Serverless messaging (Bonjour/Zeroconf)</li>
<li>Support for service discovery including nodes and search for users</li>
<li>Even more features via plugins</li>
</ul>
</description>
<screenshots>
<screenshot type="default">
<image>https://gajim.org/img/screenshots/single-window-mode.png</image>
<caption>Contact list</caption>
</screenshot>
<screenshot>
<image>https://gajim.org/img/screenshots/tabbed-chat.png</image>
<caption>Tabbed chat window</caption>
</screenshot>
<screenshot>
<image>https://gajim.org/img/screenshots/groupchat-window.png</image>
<caption>Group chat support</caption>
</screenshot>
<screenshot>
<image>https://gajim.org/img/screenshots/history-window.png</image>
<caption>Chat history</caption>
</screenshot>
<screenshot>
<image>https://gajim.org/img/screenshots/plugins.png</image>
<caption>Plugin manager</caption>
</screenshot>
</screenshots>
<launchable type="desktop-id">org.gajim.Gajim.desktop</launchable>
<developer_name>Gajim Team</developer_name>
<update_contact>gajim-devel_AT_gajim.org</update_contact>
<url type="homepage">https://gajim.org/</url>
<url type="bugtracker">https://dev.gajim.org/gajim/gajim</url>
<url type="faq">https://dev.gajim.org/gajim/gajim/-/wikis/help/gajimfaq</url>
<url type="help">https://dev.gajim.org/gajim/gajim/-/wikis/help/Help</url>
<url type="donation">https://gajim.org/development/#donations</url>
<url type="translate">https://dev.gajim.org/gajim/gajim/-/wikis/development/devtranslate</url>
<translation type="gettext">gajim</translation>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute>
</content_rating>
<releases>
<release version="1.3.3" date="2021-10-10" />
<release version="1.3.2" date="2021-04-24" />
<release version="1.3.1" date="2021-03-01" />
<release version="1.3.0" date="2021-02-08" />
<release version="1.2.2" date="2020-08-15" />
<release version="1.2.1" date="2020-07-08" />
<release version="1.2.0" date="2020-06-21" />
<release version="1.1.3" date="2019-04-23" />
<release version="1.1.2" date="2019-01-15" />
<release version="1.1.1" date="2018-12-23" />
<release version="1.1.0" date="2018-11-06" />
<release version="1.0.3" date="2018-05-20" />
<release version="1.0.2" date="2018-04-30" />
<release version="1.0.1" date="2018-04-01" />
<release version="1.0.0" date="2018-03-17" />
</releases>
</component>

View File

@ -0,0 +1,28 @@
[Desktop Entry]
Categories=Network;InstantMessaging;GTK;Chat;
Name=Gajim
GenericName=XMPP Chat Client
Comment=A fully-featured XMPP chat client
#Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
Keywords=chat;messaging;im;xmpp;bonjour;voip;
Exec=gajim %u
#Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=org.gajim.Gajim
StartupNotify=false
X-GNOME-UsesNotifications=true
Terminal=false
Type=Application
MimeType=x-scheme-handler/xmpp;
Actions=StartChat;ShowNextPendingEvent;
[Desktop Action StartChat]
Exec=gajim --start-chat
Name=Start a new chat
#Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=org.gajim.Gajim
[Desktop Action ShowNextPendingEvent]
Exec=gajim --show-next-pending-event
Name=Show next pending event
#Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=org.gajim.Gajim

12
gajim.egg-info/PKG-INFO Normal file
View File

@ -0,0 +1,12 @@
Metadata-Version: 1.2
Name: gajim
Version: 1.3.3
Summary: A GTK XMPP client
Home-page: https://gajim.org
Author: Philipp Hoerist, Yann Leboulanger
Author-email: gajim-devel@gajim.org
License: GPL v3
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.7

1043
gajim.egg-info/SOURCES.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,7 @@
[console_scripts]
gajim-remote = gajim.gajim_remote:main
[gui_scripts]
gajim = gajim.gajim:main
gajim-history-manager = gajim.history_manager:main

View File

@ -0,0 +1,10 @@
css-parser
keyring
nbxmpp>=2.0.4
packaging
precis-i18n>=1.0.0
pyOpenSSL>=16.2
pycairo>=1.16.0
[:python_version < "3.9"]
setuptools

View File

@ -0,0 +1 @@
gajim

19
gajim/__init__.py Normal file
View File

@ -0,0 +1,19 @@
import subprocess
import sys
from pathlib import Path
__version__ = "1.3.3"
IS_FLATPAK = Path('/app/share/run-as-flatpak').exists()
portable_path = Path(sys.executable).parent / 'is_portable'
IS_PORTABLE = portable_path.exists()
try:
p = subprocess.Popen('git rev-parse --short=12 HEAD', shell=True,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
node = p.communicate()[0]
if node:
__version__ += '+' + node.decode('utf-8').strip()
except Exception:
pass

325
gajim/app_actions.py Normal file
View File

@ -0,0 +1,325 @@
# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from gi.repository import Gtk
from gi.repository import Gdk
from gajim.common import app
from gajim.common import helpers
from gajim.common.app import interface
from gajim.common.exceptions import GajimGeneralException
from gajim import dialogs
from gajim.gui.dialogs import ShortcutsWindow
from gajim.gui.single_message import SingleMessageWindow
from gajim.gui.about import AboutDialog
from gajim.gui.history import HistoryWindow
from gajim.gui.discovery import ServiceDiscoveryWindow
from gajim.gui.util import open_window
from gajim.gui.util import get_app_window
# General Actions
def on_add_contact_jid(_action, param):
contact_jid = param.get_string()
open_window('AddNewContactWindow', account=None, contact_jid=contact_jid)
# Application Menu Actions
def on_preferences(_action, _param):
open_window('Preferences')
def on_plugins(_action, _param):
open_window('PluginsWindow')
def on_accounts(_action, param):
window = open_window('AccountsWindow')
account = param.get_string()
if account:
window.select_account(account)
def on_history_manager(_action, _param):
open_window('HistoryManager')
def on_bookmarks(_action, param):
account = param.get_string()
open_window('Bookmarks', account=account)
def on_quit(_action, _param):
interface.roster.on_quit_request()
def on_new_chat(_action, param):
window = open_window('StartChatDialog')
search_text = param.get_string()
if search_text:
window.set_search_text(search_text)
# Accounts Actions
def on_profile(_action, param):
account = param.get_string()
open_window('ProfileWindow', account=account)
def on_send_server_message(_action, param):
account = param.get_string()
server = app.settings.get_account_setting(account, 'hostname')
server += '/announce/online'
SingleMessageWindow(account, server, 'send')
def on_service_disco(_action, param):
account = param.get_string()
server_jid = app.settings.get_account_setting(account, 'hostname')
if server_jid in interface.instances[account]['disco']:
interface.instances[account]['disco'][server_jid].\
window.present()
else:
try:
# Object will add itself to the window dict
ServiceDiscoveryWindow(account, address_entry=True)
except GajimGeneralException:
pass
def on_create_gc(_action, param):
account = param.get_string()
open_window('CreateGroupchatWindow', account=account or None)
def on_add_contact(_action, param):
account, contact_jid = param.get_strv()
if not contact_jid:
contact_jid = None
open_window('AddNewContactWindow', account=account, contact_jid=contact_jid)
def on_single_message(_action, param):
account = param.get_string()
open_window('SingleMessageWindow', account=account, action='send')
def on_merge_accounts(action, param):
action.set_state(param)
value = param.get_boolean()
app.settings.set('mergeaccounts', value)
# Do not merge accounts if only one active
if len(app.connections) >= 2:
app.interface.roster.regroup = value
else:
app.interface.roster.regroup = False
app.interface.roster.setup_and_draw_roster()
def on_add_account(action, _param):
open_window('AccountWizard')
def on_import_contacts(_action, param):
account = param.get_string()
if 'import_contacts' in app.interface.instances:
app.interface.instances['import_contacts'].dialog.present()
else:
app.interface.instances['import_contacts'] = \
dialogs.SynchroniseSelectAccountDialog(account)
# Advanced Actions
def on_pep_config(_action, param):
account = param.get_string()
open_window('PEPConfig', account=account)
def on_mam_preferences(_action, param):
account = param.get_string()
open_window('MamPreferences', account=account)
def on_blocking_list(_action, param):
account = param.get_string()
open_window('BlockingList', account=account)
def on_history_sync(_action, param):
account = param.get_string()
open_window('HistorySyncAssistant',
account=account,
parent=interface.roster.window)
def on_server_info(_action, param):
account = param.get_string()
open_window('ServerInfo', account=account)
def on_xml_console(_action, _param):
open_window('XMLConsoleWindow')
def on_manage_proxies(_action, _param):
open_window('ManageProxies')
# Admin Actions
def on_set_motd(_action, param):
account = param.get_string()
server = app.settings.get_account_setting(account, 'hostname')
server += '/announce/motd'
SingleMessageWindow(account, server, 'send')
def on_update_motd(_action, param):
account = param.get_string()
server = app.settings.get_account_setting(account, 'hostname')
server += '/announce/motd/update'
SingleMessageWindow(account, server, 'send')
def on_delete_motd(_action, param):
account = param.get_string()
app.connections[account].get_module('Announce').delete_motd()
# Help Actions
def on_contents(_action, _param):
helpers.open_uri('https://dev.gajim.org/gajim/gajim/wikis')
def on_faq(_action, _param):
helpers.open_uri('https://dev.gajim.org/gajim/gajim/wikis/help/gajimfaq')
def on_keyboard_shortcuts(_action, _param):
ShortcutsWindow()
def on_features(_action, _param):
open_window('Features')
def on_about(_action, _param):
AboutDialog()
# View Actions
def on_file_transfers(_action, _param):
if interface.instances['file_transfers']. \
window.get_property('visible'):
interface.instances['file_transfers'].window.present()
else:
interface.instances['file_transfers'].window.show_all()
def on_history(action, param):
on_browse_history(action, param)
def on_open_event(_action, param):
dict_ = param.unpack()
app.interface.handle_event(
dict_['account'], dict_['jid'], dict_['type_'])
def on_remove_event(_action, param):
dict_ = param.unpack()
account, jid, type_ = dict_['account'], dict_['jid'], dict_['type_']
event = app.events.get_first_event(account, jid, type_)
app.events.remove_events(account, jid, event)
win = app.interface.msg_win_mgr.get_window(jid, account)
if win:
win.redraw_tab(win.get_control(jid, account))
win.show_title()
# Other Actions
def toggle_ipython(_action, _param):
"""
Show/hide the ipython window
"""
win = app.ipython_window
if win and win.window.is_visible():
win.present()
else:
app.interface.create_ipython_window()
def show_next_pending_event(_action, _param):
"""
Show the window(s) with next pending event in tabbed/group chats
"""
if app.events.get_nb_events():
account, jid, event = app.events.get_first_systray_event()
if not event:
return
app.interface.handle_event(account, jid, event.type_)
def open_mail(_action, param):
uri = param.get_string()
if not uri.startswith('mailto:'):
uri = 'mailto:%s' % uri
helpers.open_uri(uri)
def open_link(_action, param):
account, uri = param.get_strv()
helpers.open_uri(uri, account=account)
def copy_text(_action, param):
text = param.get_string()
clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clip.set_text(text, -1)
def start_chat(_action, param):
account, jid = param.get_strv()
app.interface.new_chat_from_jid(account, jid)
def on_browse_history(_action, param):
jid, account = None, None
if param is not None:
dict_ = param.unpack()
jid = dict_.get('jid')
account = dict_.get('account')
window = get_app_window(HistoryWindow)
if window is None:
HistoryWindow(jid, account)
else:
window.present()
if jid is not None and account is not None:
window.open_history(jid, account)
def on_groupchat_join(_action, param):
account, jid = param.get_strv()
open_window('GroupchatJoin', account=account, jid=jid)

562
gajim/application.py Normal file
View File

@ -0,0 +1,562 @@
# Copyright (C) 2003-2017 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
# Norman Rasmussen <norman AT rasmussen.co.za>
# Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
# Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
# James Newton <redshodan AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Julien Pivotto <roidelapluie AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2016-2017 Emmanuel Gil Peyrot <linkmauve AT linkmauve.fr>
# Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
from urllib.parse import unquote
from nbxmpp.namespaces import Namespace
from nbxmpp import JID
from nbxmpp.protocol import InvalidJid
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import Gtk
import gajim
from gajim.common import app
from gajim.common import ged
from gajim.common import configpaths
from gajim.common import logging_helpers
from gajim.common import exceptions
from gajim.common.i18n import _
from gajim.common.contacts import LegacyContactsAPI
from gajim.common.task_manager import TaskManager
from gajim.common.storage.cache import CacheStorage
from gajim.common.storage.archive import MessageArchiveStorage
from gajim.common.settings import Settings
from gajim.common.settings import LegacyConfig
class GajimApplication(Gtk.Application):
'''Main class handling activation and command line.'''
def __init__(self):
flags = (Gio.ApplicationFlags.HANDLES_COMMAND_LINE |
Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID)
Gtk.Application.__init__(self,
application_id='org.gajim.Gajim',
flags=flags)
# required to track screensaver state
self.props.register_session = True
self.add_main_option(
'version',
ord('V'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show the application\'s version'))
self.add_main_option(
'quiet',
ord('q'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show only critical errors'))
self.add_main_option(
'separate',
ord('s'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Separate profile files completely '
'(even history database and plugins)'))
self.add_main_option(
'verbose',
ord('v'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Print XML stanzas and other debug information'))
self.add_main_option(
'profile',
ord('p'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Use defined profile in configuration directory'),
'NAME')
self.add_main_option(
'config-path',
ord('c'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Set configuration directory'),
'PATH')
self.add_main_option(
'loglevel',
ord('l'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Configure logging system'),
'LEVEL')
self.add_main_option(
'warnings',
ord('w'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show all warnings'))
self.add_main_option(
'ipython',
ord('i'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Open IPython shell'))
self.add_main_option(
'gdebug',
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Sets an environment variable so '
'GLib debug messages are printed'))
self.add_main_option(
'show-next-pending-event',
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Pops up a window with the next pending event'))
self.add_main_option(
'start-chat', 0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Start a new chat'))
self.add_main_option_entries(self._get_remaining_entry())
self.connect('handle-local-options', self._handle_local_options)
self.connect('command-line', self._command_line)
self.connect('startup', self._startup)
self.interface = None
GLib.set_prgname('org.gajim.Gajim')
if GLib.get_application_name() != 'Gajim':
GLib.set_application_name('Gajim')
@staticmethod
def _get_remaining_entry():
option = GLib.OptionEntry()
option.arg = GLib.OptionArg.STRING_ARRAY
option.arg_data = None
option.arg_description = ('[URI …]')
option.flags = GLib.OptionFlags.NONE
option.long_name = GLib.OPTION_REMAINING
option.short_name = 0
return [option]
def _startup(self, _application):
# Create and initialize Application Paths & Databases
app.print_version()
app.detect_dependencies()
configpaths.create_paths()
app.settings = Settings()
app.settings.init()
app.config = LegacyConfig() # type: ignore
app.storage.cache = CacheStorage()
app.storage.cache.init()
app.storage.archive = MessageArchiveStorage()
app.storage.archive.init()
try:
app.contacts = LegacyContactsAPI()
except exceptions.DatabaseMalformed as error:
dlg = Gtk.MessageDialog(
transient_for=None,
destroy_with_parent=True,
modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=_('Database Error'))
dlg.format_secondary_text(str(error))
dlg.run()
dlg.destroy()
sys.exit()
from gajim.gui.util import load_user_iconsets
load_user_iconsets()
from gajim.common.cert_store import CertificateStore
app.cert_store = CertificateStore()
app.task_manager = TaskManager()
# Set Application Menu
app.app = self
from gajim.gui.util import get_builder
builder = get_builder('application_menu.ui')
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
from gajim.gui_interface import Interface
self.interface = Interface()
self.interface.run(self)
self.add_actions()
self._set_shortcuts()
from gajim import gui_menu_builder
gui_menu_builder.build_accounts_menu()
self.update_app_actions_state()
app.ged.register_event_handler('feature-discovered',
ged.CORE,
self._on_feature_discovered)
def _open_uris(self, uris):
accounts = list(app.connections.keys())
if not accounts:
return
for uri in uris:
app.log('uri_handler').info('open %s', uri)
if not uri.startswith('xmpp:'):
continue
# remove xmpp:
uri = uri[5:]
try:
jid, cmd = uri.split('?')
except ValueError:
# No query argument
jid, cmd = uri, 'message'
try:
jid = JID.from_string(jid)
except InvalidJid as error:
app.log('uri_handler').warning('Invalid JID %s: %s', uri, error)
continue
if cmd == 'join' and jid.resource:
app.log('uri_handler').warning('Invalid MUC JID %s', uri)
continue
jid = str(jid)
if cmd == 'join':
if len(accounts) == 1:
self.activate_action(
'groupchat-join',
GLib.Variant('as', [accounts[0], jid]))
else:
self.activate_action('start-chat', GLib.Variant('s', jid))
elif cmd == 'roster':
self.activate_action('add-contact', GLib.Variant('s', jid))
elif cmd.startswith('message'):
attributes = cmd.split(';')
message = None
for key in attributes:
if not key.startswith('body'):
continue
try:
message = unquote(key.split('=')[1])
except Exception:
app.log('uri_handler').error('Invalid URI: %s', cmd)
if len(accounts) == 1:
app.interface.new_chat_from_jid(accounts[0], jid, message)
else:
self.activate_action('start-chat', GLib.Variant('s', jid))
def do_shutdown(self, *args):
Gtk.Application.do_shutdown(self)
# Shutdown GUI and save config
if hasattr(self.interface, 'roster') and self.interface.roster:
self.interface.roster.prepare_quit()
# Commit any outstanding SQL transactions
app.storage.cache.shutdown()
app.storage.archive.shutdown()
def _command_line(self, _application, command_line):
options = command_line.get_options_dict()
remote_commands = [
('ipython', None),
('show-next-pending-event', None),
('start-chat', GLib.Variant('s', '')),
]
remaining = options.lookup_value(GLib.OPTION_REMAINING,
GLib.VariantType.new('as'))
for cmd, parameter in remote_commands:
if options.contains(cmd):
self.activate_action(cmd, parameter)
return 0
if remaining is not None:
self._open_uris(remaining.unpack())
return 0
return 0
def _handle_local_options(self,
_application: Gtk.Application,
options: GLib.VariantDict) -> int:
# Parse all options that have to be executed before ::startup
if options.contains('version'):
print(gajim.__version__)
return 0
if options.contains('profile'):
# Incorporate profile name into application id
# to have a single app instance for each profile.
profile = options.lookup_value('profile').get_string()
app_id = '%s.%s' % (self.get_application_id(), profile)
self.set_application_id(app_id)
configpaths.set_profile(profile)
if options.contains('separate'):
configpaths.set_separation(True)
if options.contains('config-path'):
path = options.lookup_value('config-path').get_string()
configpaths.set_config_root(path)
configpaths.init()
if options.contains('gdebug'):
os.environ['G_MESSAGES_DEBUG'] = 'all'
logging_helpers.init()
if options.contains('quiet'):
logging_helpers.set_quiet()
if options.contains('verbose'):
logging_helpers.set_verbose()
if options.contains('loglevel'):
loglevel = options.lookup_value('loglevel').get_string()
logging_helpers.set_loglevels(loglevel)
if options.contains('warnings'):
self.show_warnings()
return -1
@staticmethod
def show_warnings():
import traceback
import warnings
def warn_with_traceback(message, category, filename, lineno,
_file=None, line=None):
traceback.print_stack(file=sys.stderr)
sys.stderr.write(warnings.formatwarning(message, category,
filename, lineno, line))
warnings.showwarning = warn_with_traceback
warnings.filterwarnings(action="always")
def add_actions(self):
''' Build Application Actions '''
from gajim import app_actions
# General Stateful Actions
act = Gio.SimpleAction.new_stateful(
'merge', None,
GLib.Variant.new_boolean(app.settings.get('mergeaccounts')))
act.connect('change-state', app_actions.on_merge_accounts)
self.add_action(act)
actions = [
('quit', app_actions.on_quit),
('add-account', app_actions.on_add_account),
('manage-proxies', app_actions.on_manage_proxies),
('history-manager', app_actions.on_history_manager),
('preferences', app_actions.on_preferences),
('plugins', app_actions.on_plugins),
('xml-console', app_actions.on_xml_console),
('file-transfer', app_actions.on_file_transfers),
('history', app_actions.on_history),
('shortcuts', app_actions.on_keyboard_shortcuts),
('features', app_actions.on_features),
('content', app_actions.on_contents),
('about', app_actions.on_about),
('faq', app_actions.on_faq),
('ipython', app_actions.toggle_ipython),
('show-next-pending-event', app_actions.show_next_pending_event),
('start-chat', 's', app_actions.on_new_chat),
('accounts', 's', app_actions.on_accounts),
('add-contact', 's', app_actions.on_add_contact_jid),
('copy-text', 's', app_actions.copy_text),
('open-link', 'as', app_actions.open_link),
('open-mail', 's', app_actions.open_mail),
('create-groupchat', 's', app_actions.on_create_gc),
('browse-history', 'a{sv}', app_actions.on_browse_history),
('groupchat-join', 'as', app_actions.on_groupchat_join),
]
for action in actions:
if len(action) == 2:
action_name, func = action
variant = None
else:
action_name, variant, func = action
variant = GLib.VariantType.new(variant)
act = Gio.SimpleAction.new(action_name, variant)
act.connect('activate', func)
self.add_action(act)
accounts_list = sorted(app.settings.get_accounts())
if not accounts_list:
return
if len(accounts_list) > 1:
for acc in accounts_list:
self.add_account_actions(acc)
else:
self.add_account_actions(accounts_list[0])
@staticmethod
def _get_account_actions(account):
from gajim import app_actions as a
if account == 'Local':
return []
return [
('-bookmarks', a.on_bookmarks, 'online', 's'),
('-start-single-chat', a.on_single_message, 'online', 's'),
('-start-chat', a.start_chat, 'online', 'as'),
('-add-contact', a.on_add_contact, 'online', 'as'),
('-services', a.on_service_disco, 'online', 's'),
('-profile', a.on_profile, 'online', 's'),
('-server-info', a.on_server_info, 'online', 's'),
('-archive', a.on_mam_preferences, 'feature', 's'),
('-pep-config', a.on_pep_config, 'online', 's'),
('-sync-history', a.on_history_sync, 'online', 's'),
('-blocking', a.on_blocking_list, 'feature', 's'),
('-send-server-message', a.on_send_server_message, 'online', 's'),
('-set-motd', a.on_set_motd, 'online', 's'),
('-update-motd', a.on_update_motd, 'online', 's'),
('-delete-motd', a.on_delete_motd, 'online', 's'),
('-open-event', a.on_open_event, 'always', 'a{sv}'),
('-remove-event', a.on_remove_event, 'always', 'a{sv}'),
('-import-contacts', a.on_import_contacts, 'online', 's'),
]
def add_account_actions(self, account):
for action in self._get_account_actions(account):
action_name, func, state, type_ = action
action_name = account + action_name
if self.lookup_action(action_name):
# We already added this action
continue
act = Gio.SimpleAction.new(
action_name, GLib.VariantType.new(type_))
act.connect("activate", func)
if state != 'always':
act.set_enabled(False)
self.add_action(act)
def remove_account_actions(self, account):
for action in self._get_account_actions(account):
action_name = account + action[0]
self.remove_action(action_name)
def set_account_actions_state(self, account, new_state=False):
for action in self._get_account_actions(account):
action_name, _, state, _ = action
if not new_state and state in ('online', 'feature'):
# We go offline
self.lookup_action(account + action_name).set_enabled(False)
elif new_state and state == 'online':
# We go online
self.lookup_action(account + action_name).set_enabled(True)
def update_app_actions_state(self):
active_accounts = bool(app.get_connected_accounts(exclude_local=True))
self.lookup_action('create-groupchat').set_enabled(active_accounts)
enabled_accounts = app.contacts.get_accounts()
self.lookup_action('start-chat').set_enabled(enabled_accounts)
def _set_shortcuts(self):
shortcuts = {
'app.quit': ['<Primary>Q'],
'app.shortcuts': ['<Primary>question'],
'app.preferences': ['<Primary>P'],
'app.plugins': ['<Primary>E'],
'app.xml-console': ['<Primary><Shift>X'],
'app.file-transfer': ['<Primary>T'],
'app.ipython': ['<Primary><Alt>I'],
'app.start-chat::': ['<Primary>N'],
'app.create-groupchat::': ['<Primary>G'],
'win.show-roster': ['<Primary>R'],
'win.show-offline': ['<Primary>O'],
'win.show-active': ['<Primary>Y'],
'win.change-nickname': ['<Primary><Shift>N'],
'win.change-subject': ['<Primary><Shift>S'],
'win.escape': ['Escape'],
'win.browse-history': ['<Primary>H'],
'win.send-file': ['<Primary>F'],
'win.show-contact-info': ['<Primary>I'],
'win.show-emoji-chooser': ['<Primary><Shift>M'],
'win.clear-chat': ['<Primary>L'],
'win.delete-line': ['<Primary>U'],
'win.close-tab': ['<Primary>W'],
'win.move-tab-up': ['<Primary><Shift>Page_Up'],
'win.move-tab-down': ['<Primary><Shift>Page_Down'],
'win.switch-next-tab': ['<Primary>Page_Down'],
'win.switch-prev-tab': ['<Primary>Page_Up'],
'win.switch-next-unread-tab-right': ['<Primary>Tab'],
'win.switch-next-unread-tab-left': ['<Primary>ISO_Left_Tab'],
'win.switch-tab-1': ['<Alt>1', '<Alt>KP_1'],
'win.switch-tab-2': ['<Alt>2', '<Alt>KP_2'],
'win.switch-tab-3': ['<Alt>3', '<Alt>KP_3'],
'win.switch-tab-4': ['<Alt>4', '<Alt>KP_4'],
'win.switch-tab-5': ['<Alt>5', '<Alt>KP_5'],
'win.switch-tab-6': ['<Alt>6', '<Alt>KP_6'],
'win.switch-tab-7': ['<Alt>7', '<Alt>KP_7'],
'win.switch-tab-8': ['<Alt>8', '<Alt>KP_8'],
'win.switch-tab-9': ['<Alt>9', '<Alt>KP_9'],
}
for action, accels in shortcuts.items():
self.set_accels_for_action(action, accels)
def _on_feature_discovered(self, event):
if event.feature == Namespace.MAM_2:
action = '%s-archive' % event.account
self.lookup_action(action).set_enabled(True)
elif event.feature == Namespace.BLOCKING:
action = '%s-blocking' % event.account
self.lookup_action(action).set_enabled(True)

1746
gajim/chat_control.py Normal file

File diff suppressed because it is too large Load Diff

1615
gajim/chat_control_base.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
The command system providing scalable, clean and convenient architecture
in combination with declarative way of defining commands and a fair
amount of automatization for routine processes.
"""

View File

@ -0,0 +1,135 @@
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Backbone of the command system. Provides smart and controllable
dispatching mechanism with an auto-discovery functionality. In addition
to automatic discovery and dispatching, also features manual control
over the process.
"""
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from gajim.command_system.tools import remove
COMMANDS = {} # type: Dict[Any, Any]
CONTAINERS = {} # type: Dict[Any, Any]
def add_host(host):
CONTAINERS[host] = []
def remove_host(host):
remove(CONTAINERS, host)
def add_container(container):
for host in container.HOSTS:
CONTAINERS[host].append(container)
def remove_container(container):
for host in container.HOSTS:
remove(CONTAINERS[host], container)
def add_commands(container):
commands = COMMANDS.setdefault(container, {})
for command in traverse_commands(container):
for name in command.names:
commands[name] = command
def remove_commands(container):
remove(COMMANDS, container)
def traverse_commands(container):
for name in dir(container):
attribute = getattr(container, name)
if is_command(attribute):
yield attribute
def is_command(attribute):
from gajim.command_system.framework import Command
return isinstance(attribute, Command)
def is_root(namespace):
metaclass = namespace.get("__metaclass__", None)
if not metaclass:
return False
return issubclass(metaclass, Dispatchable)
def get_command(host, name):
for container in CONTAINERS[host]:
command = COMMANDS[container].get(name)
if command:
return command
def list_commands(host):
for container in CONTAINERS[host]:
commands = COMMANDS[container]
for name, command in commands.items():
yield name, command
class Dispatchable(type):
# pylint: disable=no-value-for-parameter
def __init__(cls, name, bases, namespace):
parents = super(Dispatchable, cls)
parents.__init__(name, bases, namespace)
if not is_root(namespace):
cls.dispatch()
def dispatch(cls):
if cls.AUTOMATIC:
cls.enable()
class Host(Dispatchable):
def enable(cls):
add_host(cls)
def disable(cls):
remove_host(cls)
class Container(Dispatchable):
def enable(cls):
add_container(cls)
add_commands(cls)
def disable(cls):
remove_commands(cls)
remove_container(cls)

View File

@ -0,0 +1,54 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
class BaseError(Exception):
"""
Common base for errors which relate to a specific command.
Encapsulates everything needed to identify a command, by either its
object or name.
"""
def __init__(self, message, command=None, name=None):
self.message = message
self.command = command
self.name = name
if command and not name:
self.name = command.first_name
super(BaseError, self).__init__()
def __str__(self):
return self.message
class DefinitionError(BaseError):
"""
Used to indicate errors occurred on command definition.
"""
class CommandError(BaseError):
"""
Used to indicate errors occurred during command execution.
"""
class NoCommandError(BaseError):
"""
Used to indicate an inability to find the specified command.
"""

View File

@ -0,0 +1,351 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
Provides a tiny framework with simple, yet powerful and extensible
architecture to implement commands in a straight and flexible,
declarative way.
"""
from types import FunctionType
from inspect import getargspec, getdoc
from gajim.command_system.dispatcher import Host
from gajim.command_system.dispatcher import Container
from gajim.command_system.dispatcher import get_command
from gajim.command_system.dispatcher import list_commands
from gajim.command_system.mapping import parse_arguments
from gajim.command_system.mapping import adapt_arguments
from gajim.command_system.errors import DefinitionError
from gajim.command_system.errors import CommandError
from gajim.command_system.errors import NoCommandError
class CommandHost(metaclass=Host):
"""
Command host is a hub between numerous command processors and
command containers. Aimed to participate in a dispatching process in
order to provide clean and transparent architecture.
The AUTOMATIC class variable, which must be defined by a command
host, specifies whether the command host should be automatically
dispatched and enabled by the dispatcher or not.
"""
__metaclass__ = Host
class CommandContainer(metaclass=Container):
"""
Command container is an entity which holds defined commands,
allowing them to be dispatched and processed correctly. Each
command container may be bound to a one or more command hosts.
The AUTOMATIC class variable, which must be defined by a command
processor, specifies whether the command processor should be
automatically dispatched and enabled by the dispatcher or not.
Bounding is controlled by the HOSTS class variable, which must be
defined by the command container. This variable should contain a
sequence of hosts to bound to, as a tuple or list.
"""
__metaclass__ = Container
class CommandProcessor:
"""
Command processor is an immediate command emitter. It does not
participate in the dispatching process directly, but must define a
host to bound to.
Bounding is controlled by the COMMAND_HOST variable, which must be
defined in the body of the command processor. This variable should
be set to a specific command host.
"""
# This defines a command prefix (or an initializer), which should
# precede a text in order for it to be processed as a command.
COMMAND_PREFIX = '/'
def process_as_command(self, text):
"""
Try to process text as a command. Returns True if it has been
processed as a command and False otherwise.
"""
# pylint: disable=assignment-from-no-return
prefix = text.startswith(self.COMMAND_PREFIX)
length = len(text) > len(self.COMMAND_PREFIX)
if not (prefix and length):
return False
body = text[len(self.COMMAND_PREFIX):]
body = body.strip()
parts = body.split(None, 1)
name, arguments = parts if len(parts) > 1 else (parts[0], None)
flag = self.looks_like_command(text, body, name, arguments)
if flag is not None:
return flag
self.execute_command(name, arguments)
return True
def execute_command(self, name, arguments):
cmd = self.get_command(name)
args, opts = parse_arguments(arguments) if arguments else ([], [])
args, kwargs = adapt_arguments(cmd, arguments, args, opts)
if self.command_preprocessor(cmd, name, arguments, args, kwargs):
return
value = cmd(self, *args, **kwargs)
self.command_postprocessor(cmd, name, arguments, args, kwargs, value)
def command_preprocessor(self, cmd, name, arguments, args, kwargs):
"""
Redefine this method in the subclass to execute custom code
before command gets executed.
If returns True then command execution will be interrupted and
command will not be executed.
"""
def command_postprocessor(self, cmd, name, arguments, args, kwargs, value):
"""
Redefine this method in the subclass to execute custom code
after command gets executed.
"""
def looks_like_command(self, text, body, name, arguments):
"""
This hook is being called before any processing, but after it
was determined that text looks like a command.
If returns value other then None - then further processing will
be interrupted and that value will be used to return from
process_as_command.
"""
def get_command(self, name):
cmd = get_command(self.COMMAND_HOST, name)
if not cmd:
raise NoCommandError("Command does not exist", name=name)
return cmd
def list_commands(self):
commands = list_commands(self.COMMAND_HOST)
commands = dict(commands)
return sorted(set(commands.values()), key=lambda k: k.__repr__())
class Command:
def __init__(self, handler, *names, **properties):
self.handler = handler
self.names = names
# Automatically set all the properties passed to a constructor
# by the command decorator.
for key, value in properties.items():
setattr(self, key, value)
def __call__(self, *args, **kwargs):
try:
return self.handler(*args, **kwargs)
# This allows to use a shortcut way of raising an exception
# inside a handler. That is to raise a CommandError without
# command or name attributes set. They will be set to a
# corresponding values right here in case if they was not set by
# the one who raised an exception.
except CommandError as error:
if not error.command and not error.name:
raise CommandError(error.message, self)
raise
# This one is a little bit too wide, but as Python does not have
# anything more constrained - there is no other choice. Take a
# look here if command complains about invalid arguments while
# they are ok.
except TypeError:
raise CommandError("Command received invalid arguments", self)
def __repr__(self):
return "<Command %s>" % ', '.join(self.names)
def __cmp__(self, other):
if self.first_name > other.first_name:
return 1
if self.first_name < other.first_name:
return -1
return 0
@property
def first_name(self):
return self.names[0]
@property
def native_name(self):
return self.handler.__name__
def extract_documentation(self):
"""
Extract handler's documentation which is a doc-string and
transform it to a usable format.
"""
return getdoc(self.handler)
def extract_description(self):
"""
Extract handler's description (which is a first line of the
documentation). Try to keep them simple yet meaningful.
"""
documentation = self.extract_documentation()
return documentation.split('\n', 1)[0] if documentation else None
def extract_specification(self):
"""
Extract handler's arguments specification, as it was defined
preserving their order.
"""
names, var_args, var_kwargs, defaults = getargspec(self.handler) # pylint: disable=W1505
# Behavior of this code need to be checked. Might yield
# incorrect results on some rare occasions.
spec_args = names[:-len(defaults) if defaults else len(names)]
spec_kwargs = list(
zip(names[-len(defaults):], defaults)) if defaults else {}
# Removing self from arguments specification. Command handler
# should receive the processors as a first argument, which
# should be self by the canonical means.
if spec_args.pop(0) != 'self':
raise DefinitionError("First argument must be self", self)
return spec_args, spec_kwargs, var_args, var_kwargs
def command(*names, **properties):
"""
A decorator for defining commands in a declarative way. Provides
facilities for setting command's names and properties.
Names should contain a set of names (aliases) by which the command
can be reached. If no names are given - the native name (the one
extracted from the command handler) will be used.
If native=True is given (default) and names is non-empty - then the
native name of the command will be prepended in addition to the
given names.
If usage=True is given (default) - then command help will be
appended with autogenerated usage info, based of the command handler
arguments introspection.
If source=True is given - then the first argument of the command
will receive the source arguments, as a raw, unprocessed string. The
further mapping of arguments and options will not be affected.
If raw=True is given - then command considered to be raw and should
define positional arguments only. If it defines only one positional
argument - this argument will receive all the raw and unprocessed
arguments. If the command defines more then one positional argument
- then all the arguments except the last one will be processed
normally; the last argument will get what is left after the
processing as raw and unprocessed string.
If empty=True is given - this will allow to call a raw command
without arguments.
If extra=True is given - then all the extra arguments passed to a
command will be collected into a sequence and given to the last
positional argument.
If overlap=True is given - then all the extra arguments will be
mapped as if they were values for the keyword arguments.
If expand=True is given (default) - then short, one-letter options
will be expanded to a verbose ones, based of the comparison of the
first letter. If more then one option with the same first letter is
given - then only first one will be used in the expansion.
"""
names = list(names)
native = properties.get('native', True)
usage = properties.get('usage', True)
source = properties.get('source', False)
raw = properties.get('raw', False)
empty = properties.get('empty', False)
extra = properties.get('extra', False)
overlap = properties.get('overlap', False)
expand = properties.get('expand', True)
if empty and not raw:
raise DefinitionError("Empty option can be used only with raw commands")
if extra and overlap:
raise DefinitionError("Extra and overlap options can not be used "
"together")
properties = {
'usage': usage,
'source': source,
'raw': raw,
'extra': extra,
'overlap': overlap,
'empty': empty,
'expand': expand
}
def decorator(handler):
"""
Decorator which receives handler as a first argument and then
wraps it in the command which then returns back.
"""
cmd = Command(handler, *names, **properties)
# Extract and inject a native name if either no other names are
# specified or native property is enabled, while making
# sure it is going to be the first one in the list.
if not names or native:
names.insert(0, cmd.native_name)
cmd.names = tuple(names)
return cmd
# Workaround if we are getting called without parameters. Keep in
# mind that in that case - first item in the names will be the
# handler.
if names and isinstance(names[0], FunctionType):
return decorator(names.pop(0))
return decorator
def doc(text):
"""
This decorator is used to bind a documentation (a help) to a
command.
"""
def decorator(target):
if isinstance(target, Command):
target.handler.__doc__ = text
else:
target.__doc__ = text
return target
return decorator

View File

@ -0,0 +1,20 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
The implementation and auxiliary systems which implement the standard
Gajim commands and also provide an infrastructure for adding custom
commands.
"""

View File

@ -0,0 +1,131 @@
# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
This module contains examples of how to create your own commands, by
creating a new command container, bounded to a specific command host,
and defining a set of commands inside of it.
Keep in mind that this module is not being loaded from anywhere, so the
code in here will not be executed and commands defined here will not be
detected.
"""
from gajim.common.i18n import _
from gajim.command_system.framework import CommandContainer
from gajim.command_system.framework import command
from gajim.command_system.framework import doc
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
class CustomCommonCommands(CommandContainer):
"""
The AUTOMATIC class variable, set to a positive value, instructs the
command system to automatically discover the command container and
enable it.
This command container bounds to all three available in the default
implementation command hosts. This means that commands defined in
this container will be available to all: chat, private chat and a
group chat.
"""
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
@command
def dance(self):
"""
First line of the doc string is called a description and will be
programmatically extracted and formatted.
After that you can give more help, like explanation of the
options. This one will be programmatically extracted and
formatted too.
After all the documentation - there will be autogenerated (based
on the method signature) usage information appended. You can
turn it off, if you want.
"""
return "I don't dance."
class CustomChatCommands(CommandContainer):
"""
This command container bounds only to the ChatCommands command host.
Therefore commands defined inside of the container will be available
only to a chat.
"""
AUTOMATIC = True
HOSTS = (ChatCommands,)
@command("squal", "bawl")
def sing(self):
"""
This command has an additional aliases. It means the command will
be available under three names: sing (the native name), squal
(the first alias), bawl (the second alias).
You can turn off the usage of the native name, if you want, and
specify a name or a set of names, as aliases, under which a
command will be available.
"""
return "Buy yourself a stereo."
class CustomPrivateChatCommands(CommandContainer):
"""
This command container bounds only to the PrivateChatCommands
command host. Therefore commands defined inside of the container
will be available only to a private chat.
"""
AUTOMATIC = True
HOSTS = (PrivateChatCommands,)
@command
#Example string. Do not translate
@doc(_("The same as using a doc-string, except it supports translation"))
def make_coffee(self):
return "I'm not a coffee machine!"
class CustomGroupChatCommands(CommandContainer):
"""
This command container bounds only to the GroupChatCommands command
host. Therefore commands defined inside of the container will be
available only to a group chat.
"""
AUTOMATIC = True
HOSTS = (GroupChatCommands,)
@command
def fetch(self):
return "Buy yourself a dog."

View File

@ -0,0 +1,136 @@
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Provides facilities to safely execute expressions inside a shell process
and capture the resulting output, in an asynchronous fashion, avoiding
deadlocks. If the process execution time reaches the threshold - it is
forced to terminate. Consists of a tiny framework and a couple of
commands as a frontend.
"""
from subprocess import Popen, PIPE
from os.path import expanduser
from gi.repository import GLib
from gajim.common import app
from gajim.common.i18n import _
from gajim.command_system.framework import CommandContainer
from gajim.command_system.framework import command
from gajim.command_system.framework import doc
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
class Execute(CommandContainer):
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
DIRECTORY = "~"
POLL_INTERVAL = 100
POLL_COUNT = 5
@command("exec", raw=True)
@doc(_("Execute expression inside a shell, show output"))
def execute(self, expression):
Execute.spawn(self, expression)
@classmethod
def spawn(cls, processor, expression):
command_system_execute = app.settings.get('command_system_execute')
if command_system_execute:
pipes = dict(stdout=PIPE, stderr=PIPE)
directory = expanduser(cls.DIRECTORY)
popen = Popen(expression, shell=True, cwd=directory, **pipes)
cls.monitor(processor, popen)
else:
processor.echo_error(
_('Command disabled. This command can be enabled by '
'setting \'command_system_execute\' to True in ACE '
'(Advanced Configuration Editor).'))
return
@classmethod
def monitor(cls, processor, popen):
poller = cls.poller(processor, popen)
GLib.timeout_add(cls.POLL_INTERVAL, next, poller)
@classmethod
def poller(cls, processor, popen):
for _ in range(cls.POLL_COUNT):
yield cls.brush(processor, popen)
cls.overdue(processor, popen)
yield False
@classmethod
def brush(cls, processor, popen):
if popen.poll() is not None:
cls.terminated(processor, popen)
return False
return True
@classmethod
def terminated(cls, processor, popen):
stdout, stderr = cls.fetch(popen)
success = popen.returncode == 0
if success and stdout:
processor.echo(stdout)
elif not success and stderr:
processor.echo_error(stderr)
@classmethod
def overdue(cls, processor, popen):
popen.terminate()
@classmethod
def fetch(cls, popen):
data = popen.communicate()
return map(cls.clean, data)
@staticmethod
def clean(text):
strip = chr(10) + chr(32)
return text.decode().strip(strip)
class Show(Execute):
@command("sh", raw=True)
@doc(_("Execute expression inside a shell, send output"))
def show(self, expression):
Show.spawn(self, expression)
@classmethod
def terminated(cls, processor, popen):
stdout, stderr = cls.fetch(popen)
success = popen.returncode == 0
if success and stdout:
processor.send(stdout)
elif not success and stderr:
processor.echo_error(stderr)

View File

@ -0,0 +1,45 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
The module defines a set of command hosts, which are bound to a
different command processors, which are the source of commands.
"""
from gajim.command_system.framework import CommandHost
class ChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes
commands from a chat.
"""
AUTOMATIC = True
class PrivateChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes
commands from a private chat.
"""
AUTOMATIC = True
class GroupChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes
commands from a group chat.
"""
AUTOMATIC = True

View File

@ -0,0 +1,195 @@
# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Provides a glue to tie command system framework and the actual code
where it would be dropped in. Defines a little bit of scaffolding to
support interaction between the two and a few utility methods so you
don't need to dig up the code itself to write basic commands.
"""
from traceback import print_exc
from gi.repository import Pango
from gajim.common import app
from gajim.common.i18n import _
from gajim.command_system.framework import CommandProcessor
from gajim.command_system.errors import CommandError
from gajim.command_system.errors import NoCommandError
class ChatCommandProcessor(CommandProcessor):
"""
A basic scaffolding to provide convenient interaction between the
command system and chat controls. It will be merged directly into
the controls, by ChatCommandProcessor being among superclasses of
the controls.
"""
def process_as_command(self, text):
self.command_succeeded = False
parents = super(ChatCommandProcessor, self)
flag = parents.process_as_command(text)
if flag and self.command_succeeded:
self.add_history(text)
self.clear_input()
return flag
def execute_command(self, name, arguments):
try:
parents = super(ChatCommandProcessor, self)
parents.execute_command(name, arguments)
except NoCommandError as error:
details = dict(name=error.name, message=error.message)
message = "%(name)s: %(message)s\n" % details
message += "Try using the //%(name)s or /say /%(name)s " % details
message += "construct if you intended to send it as a text."
self.echo_error(message)
except CommandError as error:
self.echo_error("%s: %s" % (error.name, error.message))
except Exception:
self.echo_error(_("Error during command execution!"))
print_exc()
else:
self.command_succeeded = True
def looks_like_command(self, text, body, name, arguments):
# Command escape stuff goes here. If text was prepended by the
# command prefix twice, like //not_a_command (if prefix is set
# to /) then it will be escaped, that is sent just as a regular
# message with one (only one) prefix removed, so message will be
# /not_a_command.
if body.startswith(self.COMMAND_PREFIX):
self.send(body)
return True
def command_preprocessor(self, command, name, arguments, args, kwargs):
# If command argument contain h or help option - forward it to
# the /help command. Don't forget to pass self, as all commands
# are unbound. And also don't forget to print output.
if 'h' in kwargs or 'help' in kwargs:
help_ = self.get_command('help')
self.echo(help_(self, name))
return True
def command_postprocessor(self, command, name, arguments, args, kwargs,
value):
# If command returns a string - print it to a user. A convenient
# and sufficient in most simple cases shortcut to a using echo.
if value and isinstance(value, str):
self.echo(value)
class CommandTools:
"""
Contains a set of basic tools and shortcuts you can use in your
commands to perform some simple operations. These will be merged
directly into the controls, by CommandTools being among superclasses
of the controls.
"""
def __init__(self):
self.install_tags()
def install_tags(self):
buffer_ = self.conv_textview.tv.get_buffer()
name = "Monospace"
font = Pango.FontDescription(name)
command_ok_tag = buffer_.create_tag("command_ok")
command_ok_tag.set_property("font-desc", font)
command_ok_tag.set_property("foreground", "#3465A4")
command_error_tag = buffer_.create_tag("command_error")
command_error_tag.set_property("font-desc", font)
command_error_tag.set_property("foreground", "#F57900")
def shift_line(self):
buffer_ = self.conv_textview.tv.get_buffer()
iter_ = buffer_.get_end_iter()
if iter_.ends_line() and not iter_.is_start():
buffer_.insert_with_tags_by_name(iter_, "\n", "eol")
def append_with_tags(self, text, *tags):
buffer_ = self.conv_textview.tv.get_buffer()
iter_ = buffer_.get_end_iter()
buffer_.insert_with_tags_by_name(iter_, text, *tags)
def echo(self, text, tag="command_ok"):
"""
Print given text to the user, as a regular command output.
"""
self.shift_line()
self.append_with_tags(text, tag)
def echo_error(self, text):
"""
Print given text to the user, as an error command output.
"""
self.echo(text, "command_error")
def send(self, text):
"""
Send a message to the contact.
"""
self.send_message(text, process_commands=False)
def set_input(self, text):
"""
Set given text into the input.
"""
buffer = self.msg_textview.get_buffer()
buffer.set_text(text)
def clear_input(self):
"""
Clear input.
"""
self.set_input(str())
def add_history(self, text):
"""
Add given text to the input history, so user can scroll through
it using ctrl + up/down arrow keys.
"""
self.save_message(text, 'sent')
@property
def connection(self):
"""
Get the current connection object.
"""
return app.connections[self.account]
@property
def full_jid(self):
"""
Get a full JID of the contact.
"""
return self.contact.get_full_jid()

View File

@ -0,0 +1,433 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
Provides an actual implementation for the standard commands.
"""
from time import localtime
from time import strftime
from datetime import date
from gi.repository import GLib
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
from gajim.common.const import KindConstant
from gajim.command_system.errors import CommandError
from gajim.command_system.framework import CommandContainer
from gajim.command_system.framework import command
from gajim.command_system.framework import doc
from gajim.command_system.mapping import generate_usage
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
class StandardCommonCommands(CommandContainer):
"""
This command container contains standard commands which are common
to all - chat, private chat, group chat.
"""
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
@command(overlap=True)
@doc(_("Show help on a given command or a list of available commands if "
"-a is given"))
def help(self, cmd=None, all_=False):
if cmd:
cmd = self.get_command(cmd)
documentation = _(cmd.extract_documentation())
usage = generate_usage(cmd)
text = []
if documentation:
text.append(documentation)
if cmd.usage:
text.append(usage)
return '\n\n'.join(text)
if all_:
for cmd_ in self.list_commands():
names = ', '.join(cmd_.names)
description = cmd_.extract_description()
self.echo("%s - %s" % (names, description))
else:
help_ = self.get_command('help')
self.echo(help_(self, 'help'))
@command(raw=True)
@doc(_("Send a message to the contact"))
def say(self, message):
self.send(message)
@command(raw=True)
@doc(_("Send action (in the third person) to the current chat"))
def me(self, action):
self.send("/me %s" % action)
@command('lastlog', overlap=True)
@doc(_("Show logged messages which mention given text"))
def grep(self, text, limit=None):
results = app.storage.archive.search_log(self.account, self.contact.jid, text)
if not results:
raise CommandError(_("%s: Nothing found") % text)
if limit:
try:
results = results[len(results) - int(limit):]
except ValueError:
raise CommandError(_("Limit must be an integer"))
for row in results:
contact = row.contact_name
if not contact:
if row.kind == KindConstant.CHAT_MSG_SENT:
contact = app.nicks[self.account]
else:
contact = self.contact.name
time_obj = localtime(row.time)
date_obj = date.fromtimestamp(row.time)
date_ = strftime('%Y-%m-%d', time_obj)
time_ = strftime('%H:%M:%S', time_obj)
if date_obj == date.today():
formatted = "[%s] %s: %s" % (time_, contact, row.message)
else:
formatted = "[%s, %s] %s: %s" % (
date_, time_, contact, row.message)
self.echo(formatted)
@command(raw=True, empty=True)
# Do not translate online, away, chat, xa, dnd
@doc(_("""
Set the current status
Status can be given as one of the following values:
online, away, chat, xa, dnd.
"""))
def status(self, status, message):
if status not in ('online', 'away', 'chat', 'xa', 'dnd'):
raise CommandError("Invalid status given")
for connection in app.connections.values():
if not app.settings.get_account_setting(connection.name,
'sync_with_global_status'):
continue
if not connection.state.is_available:
continue
connection.change_status(status, message)
@command(raw=True, empty=True)
@doc(_("Set the current status to away"))
def away(self, message):
if not message:
message = _("Away")
for connection in app.connections.values():
if not app.settings.get_account_setting(connection.name,
'sync_with_global_status'):
continue
if not connection.state.is_available:
continue
connection.change_status('away', message)
@command('back', raw=True, empty=True)
@doc(_("Set the current status to online"))
def online(self, message):
if not message:
message = _("Available")
for connection in app.connections.values():
if not app.settings.get_account_setting(connection.name,
'sync_with_global_status'):
continue
if not connection.state.is_available:
continue
connection.change_status('online', message)
@command
@doc(_("Send a disco info request"))
def disco(self):
client = app.get_client(self.account)
if not client.state.is_available:
return
client.get_module('Discovery').disco_contact(self.contact)
class StandardCommonChatCommands(CommandContainer):
"""
This command container contains standard commands, which are common
to a chat and a private chat only.
"""
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands
@command
@doc(_("Clear the text window"))
def clear(self):
self.conv_textview.clear()
@command
@doc(_("Send a ping to the contact"))
def ping(self):
if self.account == app.ZEROCONF_ACC_NAME:
raise CommandError(
_('Command is not supported for zeroconf accounts'))
app.connections[self.account].get_module('Ping').send_ping(self.contact)
@command
@doc(_("Send DTMF sequence through an open voice chat"))
def dtmf(self, sequence):
if not self.audio_sid:
raise CommandError(_("No open voice chats with the contact"))
for tone in sequence:
if not (tone in ("*", "#") or tone.isdigit()):
raise CommandError(_("%s is not a valid tone") % tone)
gjs = self.connection.get_module('Jingle').get_jingle_session
session = gjs(self.full_jid, self.audio_sid)
content = session.get_content("audio")
content.batch_dtmf(sequence)
@command
@doc(_("Toggle Voice Chat"))
def audio(self):
if not self.audio_available:
raise CommandError(_("Voice chats are not available"))
# An audio session is toggled by inverting the state of the
# appropriate button.
state = self._audio_button.get_active()
self._audio_button.set_active(not state)
@command
@doc(_("Toggle Video Chat"))
def video(self):
if not self.video_available:
raise CommandError(_("Video chats are not available"))
# A video session is toggled by inverting the state of the
# appropriate button.
state = self._video_button.get_active()
self._video_button.set_active(not state)
@command(raw=True)
@doc(_("Send a message to the contact that will attract their attention"))
def attention(self, message):
self.send_message(message, process_commands=False, attention=True)
class StandardChatCommands(CommandContainer):
"""
This command container contains standard commands which are unique
to a chat.
"""
AUTOMATIC = True
HOSTS = (ChatCommands,)
class StandardPrivateChatCommands(CommandContainer):
"""
This command container contains standard commands which are unique
to a private chat.
"""
AUTOMATIC = True
HOSTS = (PrivateChatCommands,)
class StandardGroupChatCommands(CommandContainer):
"""
This command container contains standard commands which are unique
to a group chat.
"""
AUTOMATIC = True
HOSTS = (GroupChatCommands,)
@command
@doc(_("Clear the text window"))
def clear(self):
self.conv_textview.clear()
@command(raw=True)
@doc(_("Change your nickname in a group chat"))
def nick(self, new_nick):
try:
new_nick = helpers.parse_resource(new_nick)
except Exception:
raise CommandError(_("Invalid nickname"))
# FIXME: Check state of MUC
self.connection.get_module('MUC').change_nick(
self.room_jid, new_nick)
self.new_nick = new_nick
@command('query', raw=True)
@doc(_("Open a private chat window with a specified participant"))
def chat(self, nick):
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
if nick in nicks:
self.send_pm(nick)
else:
raise CommandError(_("Nickname not found"))
@command('msg', raw=True)
@doc(_("Open a private chat window with a specified participant and send "
"him a message"))
def message(self, nick, message):
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
if nick in nicks:
self.send_pm(nick, message)
else:
raise CommandError(_("Nickname not found"))
@command(raw=True, empty=True)
@doc(_("Display or change a group chat topic"))
def topic(self, new_topic):
if new_topic:
self.connection.get_module('MUC').set_subject(
self.room_jid, new_topic)
else:
return self.subject
@command(raw=True, empty=True)
@doc(_("Invite a user to a group chat for a reason"))
def invite(self, jid, reason):
control = app.get_groupchat_control(self.account, self.room_jid)
if control is not None:
control.invite(jid)
@command(raw=True, empty=True)
@doc(_("Join a group chat given by an XMPP Address"))
def join(self, jid):
if '@' not in jid:
jid = jid + '@' + app.get_server_from_jid(self.room_jid)
app.app.activate_action(
'groupchat-join',
GLib.Variant('as', [self.account, jid]))
@command('part', 'close', raw=True, empty=True)
@doc(_("Leave the group chat, optionally giving a reason, and close tab or "
"window"))
def leave(self, reason):
self.leave(reason=reason)
@command(raw=True, empty=True)
@doc(_("""
Ban user by a nick or a JID from a groupchat
If given nickname is not found it will be treated as a JID.
"""))
def ban(self, who, reason=''):
if who in app.contacts.get_nick_list(self.account, self.room_jid):
contact = app.contacts.get_gc_contact(
self.account, self.room_jid, who)
who = contact.jid
self.connection.get_module('MUC').set_affiliation(
self.room_jid,
{who: {'affiliation': 'outcast',
'reason': reason}})
@command(raw=True, empty=True)
@doc(_("Kick user from group chat by nickname"))
def kick(self, who, reason):
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
raise CommandError(_("Nickname not found"))
self.connection.get_module('MUC').set_role(
self.room_jid, who, 'none', reason)
@command(raw=True)
# Do not translate moderator, participant, visitor, none
@doc(_("""Set participant role in group chat.
Role can be given as one of the following values:
moderator, participant, visitor, none"""))
def role(self, who, role):
if role not in ('moderator', 'participant', 'visitor', 'none'):
raise CommandError(_("Invalid role given"))
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
raise CommandError(_("Nickname not found"))
self.connection.get_module('MUC').set_role(self.room_jid, who, role)
@command(raw=True)
# Do not translate owner, admin, member, outcast, none
@doc(_("""Set participant affiliation in group chat.
Affiliation can be given as one of the following values:
owner, admin, member, outcast, none"""))
def affiliate(self, who, affiliation):
if affiliation not in ('owner', 'admin', 'member', 'outcast', 'none'):
raise CommandError(_("Invalid affiliation given"))
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
raise CommandError(_("Nickname not found"))
contact = app.contacts.get_gc_contact(self.account, self.room_jid, who)
self.connection.get_module('MUC').set_affiliation(
self.room_jid,
{contact.jid: {'affiliation': affiliation}})
@command
@doc(_("Display names of all group chat participants"))
def names(self, verbose=False):
ggc = app.contacts.get_gc_contact
gnl = app.contacts.get_nick_list
get_contact = lambda nick: ggc(self.account, self.room_jid, nick)
get_role = lambda nick: get_contact(nick).role
nicks = gnl(self.account, self.room_jid)
nicks = sorted(nicks)
nicks = sorted(nicks, key=get_role)
if not verbose:
return ", ".join(nicks)
for nick in nicks:
contact = get_contact(nick)
role = helpers.get_uf_role(contact.role)
affiliation = helpers.get_uf_affiliation(contact.affiliation)
self.echo("%s - %s - %s" % (nick, role, affiliation))
@command('ignore', raw=True)
@doc(_("Forbid a participant to send you public or private messages"))
def block(self, who):
self.on_block(None, who)
@command('unignore', raw=True)
@doc(_("Allow a participant to send you public or private messages"))
def unblock(self, who):
self.on_unblock(None, who)
@command
@doc(_("Send a ping to the contact"))
def ping(self, nick):
if self.account == app.ZEROCONF_ACC_NAME:
raise CommandError(
_('Command is not supported for zeroconf accounts'))
gc_c = app.contacts.get_gc_contact(self.account, self.room_jid, nick)
if gc_c is None:
raise CommandError(_("Unknown nickname"))
app.connections[self.account].get_module('Ping').send_ping(gc_c)

View File

@ -0,0 +1,349 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
The module contains routines to parse command arguments and map them to
the command handler's positional and keyword arguments.
Mapping is done in two stages: 1) parse arguments into positional
arguments and options; 2) adapt them to the specific command handler
according to the command properties.
"""
import re
from operator import itemgetter
from gajim.common.i18n import _
from gajim.command_system.errors import DefinitionError
from gajim.command_system.errors import CommandError
# Quite complex piece of regular expression logic to parse options and
# arguments. Might need some tweaking along the way.
ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?')
# Option keys needs to be encoded to a specific encoding as Python does
# not allow to expand dictionary with raw Unicode strings as keys from a
# **kwargs.
KEY_ENCODING = 'UTF-8'
# Defines how complete representation of command usage (generated based
# on command handler argument specification) will be rendered.
USAGE_PATTERN = 'Usage: %s %s'
def parse_arguments(arguments):
"""
Simple yet effective and sufficient in most cases parser which
parses command arguments and returns them as two lists.
First list represents positional arguments as (argument, position),
and second representing options as (key, value, position) tuples,
where position is a (start, end) span tuple of where it was found in
the string.
Options may be given in --long or -short format. As --option=value
or --option value or -option value. Keys without values will get
None as value.
Arguments and option values that contain spaces may be given as 'one
two three' or "one two three"; that is between single or double
quotes.
"""
args, opts = [], []
def intersects_opts(given_start, given_end):
"""
Check if given span intersects with any of options.
"""
for _key, _value, (start, end) in opts:
if given_start >= start and given_end <= end:
return True
return False
def intersects_args(given_start, given_end):
"""
Check if given span intersects with any of arguments.
"""
for _arg, (start, end) in args:
if given_start >= start and given_end <= end:
return True
return False
for match in re.finditer(OPT_PATTERN, arguments):
if match:
key = match.group('key')
value = match.group('value') or None
position = match.span()
opts.append((key, value, position))
for match in re.finditer(ARG_PATTERN, arguments):
if match:
body = match.group('body')
position = match.span()
args.append((body, position))
# Primitive but sufficiently effective way of disposing of
# conflicted sectors. Remove any arguments that intersect with
# options.
for arg, position in args[:]:
if intersects_opts(*position):
args.remove((arg, position))
# Primitive but sufficiently effective way of disposing of
# conflicted sectors. Remove any options that intersect with
# arguments.
for key, value, position in opts[:]:
if intersects_args(*position):
opts.remove((key, value, position))
return args, opts
def adapt_arguments(command, arguments, args, opts):
"""
Adapt args and opts got from the parser to a specific handler by
means of arguments specified on command definition. That is
transform them to *args and **kwargs suitable for passing to a
command handler.
Dashes (-) in the option names will be converted to underscores. So
you can map --one-more-option to a one_more_option=None.
If the initial value of a keyword argument is a boolean (False in
most cases) - then this option will be treated as a switch, that is
an option which does not take an argument. If a switch is followed
by an argument - then this argument will be treated just like a
normal positional argument.
"""
spec_args, spec_kwargs, var_args, _var_kwargs = command.extract_specification()
norm_kwargs = dict(spec_kwargs)
# Quite complex piece of neck-breaking logic to extract raw
# arguments if there is more, then one positional argument specified
# by the command. In case if it's just one argument which is the
# collector - this is fairly easy. But when it's more then one
# argument - the neck-breaking logic of how to retrieve residual
# arguments as a raw, all in one piece string, kicks in.
if command.raw:
if arguments:
spec_fix = 1 if command.source else 0
spec_len = len(spec_args) - spec_fix
arguments_end = len(arguments) - 1
# If there are any optional arguments given they should be
# either an unquoted positional argument or part of the raw
# argument. So we find all optional arguments that can
# possibly be unquoted argument and append them as is to the
# args.
for key, value, (start, end) in opts[:spec_len]:
if value:
end -= len(value) + 1
args.append((arguments[start:end], (start, end)))
args.append((value, (end, end + len(value) + 1)))
else:
args.append((arguments[start:end], (start, end)))
# We need in-place sort here because after manipulations
# with options order of arguments might be wrong and we just
# can't have more complex logic to not let that happen.
args.sort(key=itemgetter(1))
if spec_len > 1:
try:
_stopper, (start, end) = args[spec_len - 2]
except IndexError:
raise CommandError(_("Missing arguments"), command)
# The essential point of the whole play. After
# boundaries are being determined (supposedly correct)
# we separate raw part from the rest of arguments, which
# should be normally processed.
raw = arguments[end:]
raw = raw.strip() or None
if not raw and not command.empty:
raise CommandError(_("Missing arguments"), command)
# Discard residual arguments and all of the options as
# raw command does not support options and if an option
# is given it is rather a part of a raw argument.
args = args[:spec_len - 1]
opts = []
args.append((raw, (end, arguments_end)))
else:
# Substitute all of the arguments with only one, which
# contain raw and unprocessed arguments as a string. And
# discard all the options, as raw command does not
# support them.
args = [(arguments, (0, arguments_end))]
opts = []
else:
if command.empty:
args.append((None, (0, 0)))
else:
raise CommandError(_("Missing arguments"), command)
# The first stage of transforming options we have got to a format
# that can be used to associate them with declared keyword
# arguments. Substituting dashes (-) in their names with
# underscores (_).
for index, (key, value, position) in enumerate(opts):
if '-' in key:
opts[index] = (key.replace('-', '_'), value, position)
# The second stage of transforming options to an associable state.
# Expanding short, one-letter options to a verbose ones, if
# corresponding opt-in has been given.
if command.expand:
expanded = []
for spec_key in norm_kwargs.keys():
letter = spec_key[0] if len(spec_key) > 1 else None
if letter and letter not in expanded:
for index, (key, value, position) in enumerate(opts):
if key == letter:
expanded.append(letter)
opts[index] = (spec_key, value, position)
break
# Detect switches and set their values accordingly. If any of them
# carries a value - append it to args.
for index, (key, value, position) in enumerate(opts):
if isinstance(norm_kwargs.get(key), bool):
opts[index] = (key, True, position)
if value:
args.append((value, position))
# Sorting arguments and options (just to be sure) in regarding to
# their positions in the string.
args.sort(key=itemgetter(1))
opts.sort(key=itemgetter(2))
# Stripping down position information supplied with arguments and
# options as it won't be needed again.
args = list(map(lambda t: t[0], args))
opts = list(map(lambda t: (t[0], t[1]), opts))
# If command has extra option enabled - collect all extra arguments
# and pass them to a last positional argument command defines as a
# list.
if command.extra:
if not var_args:
spec_fix = 1 if not command.source else 2
spec_len = len(spec_args) - spec_fix
extra = args[spec_len:]
args = args[:spec_len]
args.append(extra)
else:
raise DefinitionError("Can not have both, extra and *args")
# Detect if positional arguments overlap keyword arguments. If so
# and this is allowed by command options - then map them directly to
# their options, so they can get proper further processing.
spec_fix = 1 if command.source else 0
spec_len = len(spec_args) - spec_fix
if len(args) > spec_len:
if command.overlap:
overlapped = args[spec_len:]
args = args[:spec_len]
for arg, spec_key, _spec_value in zip(overlapped, spec_kwargs):
opts.append((spec_key, arg))
else:
raise CommandError(_("Too many arguments"), command)
# Detect every switch and ensure it will not receive any arguments.
# Normally this does not happen unless overlapping is enabled.
for key, value in opts:
initial = norm_kwargs.get(key)
if isinstance(initial, bool):
if not isinstance(value, bool):
raise CommandError(
"%s: Switch can not take an argument" % key, command)
# Inject the source arguments as a string as a first argument, if
# command has enabled the corresponding option.
if command.source:
args.insert(0, arguments)
# Return *args and **kwargs in the form suitable for passing to a
# command handler and being expanded.
return tuple(args), dict(opts)
def generate_usage(command, complete=True):
"""
Extract handler's arguments specification and wrap them in a
human-readable format usage information. If complete is given - then
USAGE_PATTERN will be used to render the specification completely.
"""
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
# Remove some special positional arguments from the specification,
# but store their names so they can be used for usage info
# generation.
_sp_source = spec_args.pop(0) if command.source else None
sp_extra = spec_args.pop() if command.extra else None
kwargs = []
letters = []
for key, value in spec_kwargs:
letter = key[0]
key = key.replace('_', '-')
if isinstance(value, bool):
value = str()
else:
value = '=%s' % value
if letter not in letters:
kwargs.append('-(-%s)%s%s' % (letter, key[1:], value))
letters.append(letter)
else:
kwargs.append('--%s%s' % (key, value))
usage = str()
args = str()
if command.raw:
spec_len = len(spec_args) - 1
if spec_len:
args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' '
args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1]
else:
if spec_args:
args += '<%s>' % ', '.join(spec_args)
if var_args or sp_extra:
args += (' ' if spec_args else str()) + '<<%s>>' % (
var_args or sp_extra)
usage += args
if kwargs or var_kwargs:
if kwargs:
usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs)
if var_kwargs:
usage += (' ' if args else str()) + '[[%s]]' % var_kwargs
# Native name will be the first one if it is included. Otherwise,
# names will be in the order they were specified.
if len(command.names) > 1:
names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:]))
else:
names = command.first_name
return USAGE_PATTERN % (names, usage) if complete else usage

View File

@ -0,0 +1,34 @@
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
def remove(sequence, target):
if isinstance(sequence, list):
if target in sequence:
sequence.remove(target)
elif isinstance(sequence, dict):
if target in sequence:
del sequence[target]

0
gajim/common/__init__.py Normal file
View File

32
gajim/common/account.py Normal file
View File

@ -0,0 +1,32 @@
# Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
class Account:
def __init__(self, name, contacts, gc_contacts):
self.name = name
self.contacts = contacts
self.gc_contacts = gc_contacts
def change_contact_jid(self, old_jid, new_jid):
self.contacts.change_contact_jid(old_jid, new_jid)
def __repr__(self):
return self.name
def __hash__(self):
return hash(self.name)

691
gajim/common/app.py Normal file
View File

@ -0,0 +1,691 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
# Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2018 Philipp Hörist <philipp @ hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
from typing import Optional # pylint: disable=unused-import
from typing import cast
import os
import sys
import logging
import uuid
from collections import namedtuple
from collections import defaultdict
import nbxmpp
from gi.repository import Gdk
import gajim
from gajim.common import config as c_config
from gajim.common import configpaths
from gajim.common import ged as ged_module
from gajim.common.i18n import LANG
from gajim.common.const import Display
from gajim.common.events import Events
from gajim.common.types import NetworkEventsControllerT # pylint: disable=unused-import
from gajim.common.types import InterfaceT # pylint: disable=unused-import
from gajim.common.types import ConnectionT # pylint: disable=unused-import
from gajim.common.types import LegacyContactsAPIT # pylint: disable=unused-import
from gajim.common.types import SettingsT # pylint: disable=unused-import
interface = cast(InterfaceT, None)
thread_interface = lambda *args: None # Interface to run a thread and then a callback
config = c_config.Config()
settings = cast(SettingsT, None)
version = gajim.__version__
connections = {} # type: Dict[str, ConnectionT]
avatar_cache = {} # type: Dict[str, Dict[str, Any]]
bob_cache = {} # type: Dict[str, bytes]
ipython_window = None
app = None # Gtk.Application
ged = ged_module.GlobalEventsDispatcher() # Global Events Dispatcher
nec = cast(NetworkEventsControllerT, None)
plugin_manager = None # Plugins Manager
class Storage:
def __init__(self):
self.cache = None
self.archive = None
storage = Storage()
css_config = None
transport_type = {} # type: Dict[str, str]
# dict of time of the latest incoming message per jid
# {acct1: {jid1: time1, jid2: time2}, }
last_message_time = {} # type: Dict[str, Dict[str, float]]
contacts = cast(LegacyContactsAPIT, None)
# tell if we are connected to the room or not
# {acct: {room_jid: True}}
gc_connected = {} # type: Dict[str, Dict[str, bool]]
# dict of the pass required to enter a room
# {room_jid: password}
gc_passwords = {} # type: Dict[str, str]
# dict of rooms that must be automatically configured
# and for which we have a list of invities
# {account: {room_jid: {'invities': []}}}
automatic_rooms = {} # type: Dict[str, Dict[str, Dict[str, List[str]]]]
# dict of groups, holds if they are expanded or not
groups = {} # type: Dict[str, Dict[str, Dict[str, bool]]]
# list of contacts that has just signed in
newly_added = {} # type: Dict[str, List[str]]
# list of contacts that has just signed out
to_be_removed = {} # type: Dict[str, List[str]]
events = Events()
notification = None
# list of our nick names in each account
nicks = {} # type: Dict[str, str]
# should we block 'contact signed in' notifications for this account?
# this is only for the first 30 seconds after we change our show
# to something else than offline
# can also contain account/transport_jid to block notifications for contacts
# from this transport
block_signed_in_notifications = {} # type: Dict[str, bool]
proxy65_manager = None
cert_store = None
task_manager = None
# zeroconf account name
ZEROCONF_ACC_NAME = 'Local'
# These will be set in app.gui_interface.
idlequeue = None # type: nbxmpp.idlequeue.IdleQueue
socks5queue = None
gupnp_igd = None
gsound_ctx = None
_dependencies = {
'AVAHI': False,
'PYBONJOUR': False,
'FARSTREAM': False,
'GST': False,
'AV': False,
'GEOCLUE': False,
'UPNP': False,
'GSOUND': False,
'GSPELL': False,
'IDLE': False,
}
_tasks = defaultdict(list) # type: Dict[int, List[Any]]
def print_version():
log('gajim').info('Gajim Version: %s', gajim.__version__)
def get_client(account):
return connections[account]
def is_installed(dependency):
if dependency == 'ZEROCONF':
# Alias for checking zeroconf libs
return _dependencies['AVAHI'] or _dependencies['PYBONJOUR']
return _dependencies[dependency]
def is_flatpak():
return gajim.IS_FLATPAK
def is_portable():
return gajim.IS_PORTABLE
def is_display(display):
# XWayland reports as Display X11, so try with env var
is_wayland = os.environ.get('XDG_SESSION_TYPE') == 'wayland'
if is_wayland and display == Display.WAYLAND:
return True
default = Gdk.Display.get_default()
if default is None:
log('gajim').warning('Could not determine window manager')
return False
return default.__class__.__name__ == display.value
def disable_dependency(dependency):
_dependencies[dependency] = False
def detect_dependencies():
import gi
# ZEROCONF
try:
import pybonjour # pylint: disable=unused-import
_dependencies['PYBONJOUR'] = True
except Exception:
pass
try:
gi.require_version('Avahi', '0.6')
from gi.repository import Avahi # pylint: disable=unused-import
_dependencies['AVAHI'] = True
except Exception:
pass
try:
gi.require_version('Gst', '1.0')
from gi.repository import Gst
_dependencies['GST'] = True
except Exception:
pass
try:
gi.require_version('Farstream', '0.2')
from gi.repository import Farstream
_dependencies['FARSTREAM'] = True
except Exception:
pass
try:
if _dependencies['GST'] and _dependencies['FARSTREAM']:
Gst.init(None)
conference = Gst.ElementFactory.make('fsrtpconference', None)
conference.new_session(Farstream.MediaType.AUDIO)
from gajim.gui.gstreamer import create_gtk_widget
sink, _, _ = create_gtk_widget()
if sink is not None:
_dependencies['AV'] = True
except Exception as error:
log('gajim').warning('AV dependency test failed: %s', error)
# GEOCLUE
try:
gi.require_version('Geoclue', '2.0')
from gi.repository import Geoclue # pylint: disable=unused-import
_dependencies['GEOCLUE'] = True
except (ImportError, ValueError):
pass
# UPNP
try:
gi.require_version('GUPnPIgd', '1.0')
from gi.repository import GUPnPIgd
global gupnp_igd
gupnp_igd = GUPnPIgd.SimpleIgd()
_dependencies['UPNP'] = True
except ValueError:
pass
# IDLE
try:
from gajim.common import idle
if idle.Monitor.is_available():
_dependencies['IDLE'] = True
except Exception:
pass
# GSOUND
try:
gi.require_version('GSound', '1.0')
from gi.repository import GLib
from gi.repository import GSound
global gsound_ctx
gsound_ctx = GSound.Context()
try:
gsound_ctx.init()
_dependencies['GSOUND'] = True
except GLib.Error as error:
log('gajim').warning('GSound init failed: %s', error)
except (ImportError, ValueError):
pass
# GSPELL
try:
gi.require_version('Gspell', '1')
from gi.repository import Gspell
langs = Gspell.language_get_available()
for lang in langs:
log('gajim').info('%s (%s) dict available',
lang.get_name(), lang.get_code())
if langs:
_dependencies['GSPELL'] = True
except (ImportError, ValueError):
pass
# Print results
for dep, val in _dependencies.items():
log('gajim').info('%-13s %s', dep, val)
log('gajim').info('Used language: %s', LANG)
def detect_desktop_env():
if sys.platform in ('win32', 'darwin'):
return sys.platform
desktop = os.environ.get('XDG_CURRENT_DESKTOP')
if desktop is None:
return None
if 'gnome' in desktop.lower():
return 'gnome'
return desktop
desktop_env = detect_desktop_env()
def get_an_id():
return str(uuid.uuid4())
def get_nick_from_jid(jid):
pos = jid.find('@')
return jid[:pos]
def get_server_from_jid(jid):
pos = jid.find('@') + 1 # after @
return jid[pos:]
def get_name_and_server_from_jid(jid):
name = get_nick_from_jid(jid)
server = get_server_from_jid(jid)
return name, server
def get_room_and_nick_from_fjid(jid):
# fake jid is the jid for a contact in a room
# gaim@conference.jabber.no/nick/nick-continued
# return ('gaim@conference.jabber.no', 'nick/nick-continued')
l = jid.split('/', 1)
if len(l) == 1: # No nick
l.append('')
return l
def get_real_jid_from_fjid(account, fjid):
"""
Return real jid or returns None, if we don't know the real jid
"""
room_jid, nick = get_room_and_nick_from_fjid(fjid)
if not nick: # It's not a fake_jid, it is a real jid
return fjid # we return the real jid
real_jid = fjid
if interface.msg_win_mgr.get_gc_control(room_jid, account):
# It's a pm, so if we have real jid it's in contact.jid
gc_contact = contacts.get_gc_contact(account, room_jid, nick)
if not gc_contact:
return
# gc_contact.jid is None when it's not a real jid (we don't know real jid)
real_jid = gc_contact.jid
return real_jid
def get_room_from_fjid(jid):
return get_room_and_nick_from_fjid(jid)[0]
def get_contact_name_from_jid(account, jid):
c = contacts.get_first_contact_from_jid(account, jid)
return c.name
def get_jid_without_resource(jid):
return jid.split('/')[0]
def construct_fjid(room_jid, nick):
# fake jid is the jid for a contact in a room
# gaim@conference.jabber.org/nick
return room_jid + '/' + nick
def get_resource_from_jid(jid):
jids = jid.split('/', 1)
if len(jids) > 1:
return jids[1] # abc@doremi.org/res/res-continued
return ''
def get_number_of_accounts():
"""
Return the number of ALL accounts
"""
return len(connections.keys())
def get_number_of_connected_accounts(accounts_list=None):
"""
Returns the number of CONNECTED accounts. Uou can optionally pass an
accounts_list and if you do those will be checked, else all will be checked
"""
connected_accounts = 0
if accounts_list is None:
accounts = connections.keys()
else:
accounts = accounts_list
for account in accounts:
if account_is_connected(account):
connected_accounts = connected_accounts + 1
return connected_accounts
def get_available_clients():
clients = []
for client in connections.values():
if client.state.is_available:
clients.append(client)
return clients
def get_connected_accounts(exclude_local=False):
"""
Returns a list of CONNECTED accounts
"""
account_list = []
for account in connections:
if account == 'Local' and exclude_local:
continue
if account_is_connected(account):
account_list.append(account)
return account_list
def get_accounts_sorted():
'''
Get all accounts alphabetically sorted with Local first
'''
account_list = settings.get_accounts()
account_list.sort(key=str.lower)
if 'Local' in account_list:
account_list.remove('Local')
account_list.insert(0, 'Local')
return account_list
def get_enabled_accounts_with_labels(exclude_local=True, connected_only=False,
private_storage_only=False):
"""
Returns a list with [account, account_label] entries.
Order by account_label
"""
accounts = []
for acc in connections:
if exclude_local and account_is_zeroconf(acc):
continue
if connected_only and not account_is_connected(acc):
continue
if private_storage_only and not account_supports_private_storage(acc):
continue
accounts.append([acc, get_account_label(acc)])
accounts.sort(key=lambda xs: str.lower(xs[1]))
return accounts
def get_account_label(account):
return settings.get_account_setting(account, 'account_label') or account
def account_is_zeroconf(account):
return connections[account].is_zeroconf
def account_supports_private_storage(account):
# If Delimiter module is not available we can assume
# Private Storage is not available
return connections[account].get_module('Delimiter').available
def account_is_connected(account):
if account not in connections:
return False
return (connections[account].state.is_connected or
connections[account].state.is_available)
def account_is_available(account):
if account not in connections:
return False
return connections[account].state.is_available
def account_is_disconnected(account):
return not account_is_connected(account)
def zeroconf_is_connected():
return account_is_connected(ZEROCONF_ACC_NAME) and \
settings.get_account_setting(ZEROCONF_ACC_NAME, 'is_zeroconf')
def in_groupchat(account, room_jid):
room_jid = str(room_jid)
if room_jid not in gc_connected[account]:
return False
return gc_connected[account][room_jid]
def get_transport_name_from_jid(jid, use_config_setting=True):
"""
Returns 'gg', 'irc' etc
If JID is not from transport returns None.
"""
#FIXME: jid can be None! one TB I saw had this problem:
# in the code block # it is a groupchat presence in handle_event_notify
# jid was None. Yann why?
if not jid or (use_config_setting and not config.get('use_transports_iconsets')):
return
host = get_server_from_jid(jid)
if host in transport_type:
return transport_type[host]
# host is now f.e. icq.foo.org or just icq (sometimes on hacky transports)
host_splitted = host.split('.')
if host_splitted:
# now we support both 'icq.' and 'icq' but not icqsucks.org
host = host_splitted[0]
if host in ('irc', 'icq', 'sms', 'weather', 'mrim', 'facebook'):
return host
if host == 'gg':
return 'gadu-gadu'
if host == 'jit':
return 'icq'
if host == 'facebook':
return 'facebook'
return None
def jid_is_transport(jid):
# if not '@' or '@' starts the jid then it is transport
if jid.find('@') <= 0:
return True
return False
def get_jid_from_account(account_name):
"""
Return the jid we use in the given account
"""
name = settings.get_account_setting(account_name, 'name')
hostname = settings.get_account_setting(account_name, 'hostname')
jid = name + '@' + hostname
return jid
def get_account_from_jid(jid):
for account in settings.get_accounts():
if jid == get_jid_from_account(account):
return account
def get_our_jids():
"""
Returns a list of the jids we use in our accounts
"""
our_jids = list()
for account in contacts.get_accounts():
our_jids.append(get_jid_from_account(account))
return our_jids
def get_hostname_from_account(account_name, use_srv=False):
"""
Returns hostname (if custom hostname is used, that is returned)
"""
if use_srv and connections[account_name].connected_hostname:
return connections[account_name].connected_hostname
if settings.get_account_setting(account_name, 'use_custom_host'):
return settings.get_account_setting(account_name, 'custom_host')
return settings.get_account_setting(account_name, 'hostname')
def get_notification_image_prefix(jid):
"""
Returns the prefix for the notification images
"""
transport_name = get_transport_name_from_jid(jid)
if transport_name in ('icq', 'facebook'):
prefix = transport_name
else:
prefix = 'jabber'
return prefix
def get_name_from_jid(account, jid):
"""
Return from JID's shown name and if no contact returns jids
"""
contact = contacts.get_first_contact_from_jid(account, jid)
if contact:
actor = contact.get_shown_name()
else:
actor = jid
return actor
def get_recent_groupchats(account):
recent_groupchats = settings.get_account_setting(
account, 'recent_groupchats').split()
RecentGroupchat = namedtuple('RecentGroupchat',
['room', 'server', 'nickname'])
recent_list = []
for groupchat in recent_groupchats:
jid = nbxmpp.JID.from_string(groupchat)
recent = RecentGroupchat(jid.localpart, jid.domain, jid.resource)
recent_list.append(recent)
return recent_list
def add_recent_groupchat(account, room_jid, nickname):
recent = settings.get_account_setting(
account, 'recent_groupchats').split()
full_jid = room_jid + '/' + nickname
if full_jid in recent:
recent.remove(full_jid)
recent.insert(0, full_jid)
if len(recent) > 10:
recent = recent[0:9]
config_value = ' '.join(recent)
settings.set_account_setting(account, 'recent_groupchats', config_value)
def get_priority(account, show):
"""
Return the priority an account must have
"""
if not show:
show = 'online'
if show in ('online', 'chat', 'away', 'xa', 'dnd') and \
settings.get_account_setting(account, 'adjust_priority_with_status'):
prio = settings.get_account_setting(account, 'autopriority_' + show)
else:
prio = settings.get_account_setting(account, 'priority')
if prio < -128:
prio = -128
elif prio > 127:
prio = 127
return prio
def log(domain):
if domain != 'gajim':
domain = 'gajim.%s' % domain
return logging.getLogger(domain)
def prefers_app_menu():
if sys.platform == 'darwin':
return True
if sys.platform == 'win32':
return False
return app.prefers_app_menu()
def load_css_config():
global css_config
from gajim.gui.css_config import CSSConfig
css_config = CSSConfig()
def set_debug_mode(enable: bool) -> None:
debug_folder = configpaths.get('DEBUG')
debug_enabled = debug_folder / 'debug-enabled'
if enable:
debug_enabled.touch()
else:
if debug_enabled.exists():
debug_enabled.unlink()
def get_debug_mode() -> bool:
debug_folder = configpaths.get('DEBUG')
debug_enabled = debug_folder / 'debug-enabled'
return debug_enabled.exists()
def get_stored_bob_data(algo_hash: str) -> Optional[bytes]:
try:
return bob_cache[algo_hash]
except KeyError:
filepath = configpaths.get('BOB') / algo_hash
if filepath.exists():
with open(str(filepath), 'r+b') as file:
data = file.read()
return data
return None
def get_groupchat_control(account, jid):
control = app.interface.msg_win_mgr.get_gc_control(jid, account)
if control is not None:
return control
try:
return app.interface.minimized_controls[account][jid]
except Exception:
return None
def register_task(self, task):
_tasks[id(self)].append(task)
def remove_task(task, id_):
try:
_tasks[id_].remove(task)
except Exception:
pass
else:
if not _tasks[id_]:
del _tasks[id_]
def cancel_tasks(obj):
id_ = id(obj)
if id_ not in _tasks:
return
task_list = _tasks[id_]
for task in task_list:
task.cancel()

View File

@ -0,0 +1,89 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
from gi.repository import GLib
from gi.repository import Gio
from gajim.common import configpaths
from gajim.common.helpers import get_random_string
from gajim.common.helpers import write_file_async
log = logging.getLogger('gajim.c.cert_store')
class CertificateStore:
def __init__(self):
self._path = configpaths.get('CERT_STORE')
self._certs = []
self._load_certificates()
def _get_random_path(self):
filename = get_random_string()
path = self._path / filename
if path.exists():
return self._get_random_path()
return path
def _load_certificates(self):
for path in self._path.iterdir():
if path.is_dir():
continue
try:
cert = Gio.TlsCertificate.new_from_file(str(path))
except GLib.Error as error:
log.warning('Can\'t load certificate: %s, %s', path, error)
continue
log.info('Loaded: %s', path.stem)
self._certs.append(cert)
log.info('%s Certificates loaded', len(self._certs))
def get_certificates(self):
return list(self._certs)
def add_certificate(self, certificate):
log.info('Add certificate to trust store')
self._certs.append(certificate)
pem = certificate.props.certificate_pem
path = self._get_random_path()
write_file_async(path,
pem.encode(),
self._on_certificate_write_finished,
path)
def verify(self, certificate, tls_errors):
if Gio.TlsCertificateFlags.UNKNOWN_CA in tls_errors:
for trusted_certificate in self._certs:
if trusted_certificate.is_same(certificate):
tls_errors.remove(Gio.TlsCertificateFlags.UNKNOWN_CA)
break
if not tls_errors:
return True
return False
@staticmethod
def _on_certificate_write_finished(data, error, path):
if data is None:
log.error('Can\'t store certificate: %s', error)
return
log.info('Certificate stored: %s', path)

642
gajim/common/client.py Normal file
View File

@ -0,0 +1,642 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import nbxmpp
from nbxmpp.client import Client as NBXMPPClient
from nbxmpp.const import StreamError
from nbxmpp.const import ConnectionType
from gi.repository import GLib
from gajim.common import passwords
from gajim.common.nec import NetworkEvent
from gajim.common import app
from gajim.common import helpers
from gajim.common import modules
from gajim.common.const import ClientState
from gajim.common.helpers import get_custom_host
from gajim.common.helpers import get_user_proxy
from gajim.common.helpers import warn_about_plain_connection
from gajim.common.helpers import get_resource
from gajim.common.helpers import get_idle_status_message
from gajim.common.idle import Monitor
from gajim.common.i18n import _
from gajim.common.connection_handlers import ConnectionHandlers
from gajim.common.connection_handlers_events import MessageSentEvent
from gajim.gui.util import open_window
log = logging.getLogger('gajim.client')
class Client(ConnectionHandlers):
def __init__(self, account):
self._client = None
self._account = account
self.name = account
self._hostname = app.settings.get_account_setting(self._account,
'hostname')
self._user = app.settings.get_account_setting(self._account, 'name')
self.password = None
self._priority = 0
self._connect_machine_calls = 0
self.addressing_supported = False
self.is_zeroconf = False
self.pep = {}
self.roster_supported = True
self._state = ClientState.DISCONNECTED
self._status_sync_on_resume = False
self._status = 'online'
self._status_message = ''
self._idle_status = 'online'
self._idle_status_enabled = True
self._idle_status_message = ''
self._reconnect = True
self._reconnect_timer_source = None
self._destroy_client = False
self._remove_account = False
self._destroyed = False
self.available_transports = {}
modules.register_modules(self)
self._create_client()
if Monitor.is_available():
self._idle_handler_id = Monitor.connect('state-changed',
self._idle_state_changed)
self._screensaver_handler_id = app.app.connect(
'notify::screensaver-active', self._screensaver_state_changed)
ConnectionHandlers.__init__(self)
def _set_state(self, state):
log.info('State: %s', state)
self._state = state
@property
def state(self):
return self._state
@property
def account(self):
return self._account
@property
def status(self):
return self._status
@property
def status_message(self):
if self._idle_status_active():
return self._idle_status_message
return self._status_message
@property
def priority(self):
return self._priority
@property
def certificate(self):
return self._client.peer_certificate[0]
@property
def features(self):
return self._client.features
@property
def local_address(self):
address = self._client.local_address
if address is not None:
return address.to_string().split(':')[0]
return None
def set_remove_account(self, value):
# Used by the RemoveAccount Assistant to make the Client
# not react to any stream errors that happen while the
# account is removed by the server and the connection is killed
self._remove_account = value
def _create_client(self):
if self._destroyed:
# If we disable an account cleanup() is called and all
# modules are unregistered. Because disable_account() does not wait
# for the client to properly disconnect, handlers of the
# nbxmpp.Client() are emitted after we called cleanup().
# After nbxmpp.Client() disconnects and is destroyed we create a
# new instance with this method but modules.get_handlers() fails
# because modules are already unregistered.
# TODO: Make this nicer
return
log.info('Create new nbxmpp client')
self._client = NBXMPPClient(log_context=self._account)
self.connection = self._client
self._client.set_domain(self._hostname)
self._client.set_username(self._user)
self._client.set_resource(get_resource(self._account))
pass_saved = app.settings.get_account_setting(self._account, 'savepass')
if pass_saved:
# Request password from keyring only if the user chose to save
# his password
self.password = passwords.get_password(self._account)
self._client.set_password(self.password)
self._client.set_accepted_certificates(
app.cert_store.get_certificates())
self._client.subscribe('resume-failed', self._on_resume_failed)
self._client.subscribe('resume-successful', self._on_resume_successful)
self._client.subscribe('disconnected', self._on_disconnected)
self._client.subscribe('connection-failed', self._on_connection_failed)
self._client.subscribe('connected', self._on_connected)
self._client.subscribe('stanza-sent', self._on_stanza_sent)
self._client.subscribe('stanza-received', self._on_stanza_received)
for handler in modules.get_handlers(self):
self._client.register_handler(handler)
def _on_resume_failed(self, _client, _signal_name):
log.info('Resume failed')
app.nec.push_incoming_event(NetworkEvent(
'our-show', account=self._account, show='offline'))
self.get_module('Chatstate').enabled = False
def _on_resume_successful(self, _client, _signal_name):
self._set_state(ClientState.CONNECTED)
self._set_client_available()
if self._status_sync_on_resume:
self._status_sync_on_resume = False
self.update_presence()
else:
# Normally show is updated when we receive a presence reflection.
# On resume, if show has not changed while offline, we dont send
# a new presence so we have to trigger the event here.
app.nec.push_incoming_event(
NetworkEvent('our-show',
account=self._account,
show=self._status))
def _set_client_available(self):
self._set_state(ClientState.AVAILABLE)
app.nec.push_incoming_event(NetworkEvent('account-connected',
account=self._account))
def disconnect(self, gracefully, reconnect, destroy_client=False):
if self._state.is_disconnecting:
log.warning('Disconnect already in progress')
return
self._set_state(ClientState.DISCONNECTING)
self._reconnect = reconnect
self._destroy_client = destroy_client
log.info('Starting to disconnect %s', self._account)
self._client.disconnect(immediate=not gracefully)
def _on_disconnected(self, _client, _signal_name):
log.info('Disconnect %s', self._account)
self._set_state(ClientState.DISCONNECTED)
domain, error, text = self._client.get_error()
if self._remove_account:
# Account was removed via RemoveAccount Assistant.
self._reconnect = False
elif domain == StreamError.BAD_CERTIFICATE:
self._reconnect = False
self._destroy_client = True
cert, errors = self._client.peer_certificate
open_window('SSLErrorDialog',
account=self._account,
client=self,
cert=cert,
error=errors.pop())
elif domain in (StreamError.STREAM, StreamError.BIND):
if error == 'conflict':
# Reset resource
app.settings.set_account_setting(self._account,
'resource',
'gajim.$rand')
elif domain == StreamError.SASL:
self._reconnect = False
self._destroy_client = True
if error in ('not-authorized', 'no-password'):
def _on_password(password):
self.password = password
self._client.set_password(password)
self._prepare_for_connect()
app.nec.push_incoming_event(NetworkEvent(
'password-required', conn=self, on_password=_on_password))
app.nec.push_incoming_event(
NetworkEvent('simple-notification',
account=self._account,
type_='connection-failed',
title=_('Authentication failed'),
text=text or error))
if self._reconnect:
self._after_disconnect()
self._schedule_reconnect()
app.nec.push_incoming_event(
NetworkEvent('our-show', account=self._account, show='error'))
else:
self.get_module('Chatstate').enabled = False
app.nec.push_incoming_event(NetworkEvent(
'our-show', account=self._account, show='offline'))
self._after_disconnect()
def _after_disconnect(self):
self._disable_reconnect_timer()
self.get_module('VCardAvatars').avatar_advertised = False
app.proxy65_manager.disconnect(self._client)
self.terminate_sessions()
self.get_module('Bytestream').remove_all_transfers()
if self._destroy_client:
self._client.destroy()
self._client = None
self._destroy_client = False
self._create_client()
app.nec.push_incoming_event(NetworkEvent('account-disconnected',
account=self._account))
def _on_connection_failed(self, _client, _signal_name):
self._schedule_reconnect()
def _on_connected(self, _client, _signal_name):
self._set_state(ClientState.CONNECTED)
self.get_module('MUC').get_manager().reset_state()
self.get_module('Discovery').discover_server_info()
self.get_module('Discovery').discover_account_info()
self.get_module('Discovery').discover_server_items()
self.get_module('Chatstate').enabled = True
self.get_module('MAM').reset_state()
def _on_stanza_sent(self, _client, _signal_name, stanza):
app.nec.push_incoming_event(NetworkEvent('stanza-sent',
account=self._account,
stanza=stanza))
def _on_stanza_received(self, _client, _signal_name, stanza):
app.nec.push_incoming_event(NetworkEvent('stanza-received',
account=self._account,
stanza=stanza))
def get_own_jid(self):
"""
Return the last full JID we received on a bind event.
In case we were never connected it returns the bare JID from config.
"""
if self._client is not None:
jid = self._client.get_bound_jid()
if jid is not None:
return jid
# This returns the bare jid
return nbxmpp.JID.from_string(app.get_jid_from_account(self._account))
def change_status(self, show, message):
if not message:
message = ''
self._idle_status_enabled = show == 'online'
self._status_message = message
if show != 'offline':
self._status = show
if self._state.is_disconnecting:
log.warning('Can\'t change status while '
'disconnect is in progress')
return
if self._state.is_disconnected:
if show == 'offline':
return
self._prepare_for_connect()
return
if self._state.is_connecting:
if show == 'offline':
self.disconnect(gracefully=False,
reconnect=False,
destroy_client=True)
return
if self._state.is_reconnect_scheduled:
if show == 'offline':
self._destroy_client = True
self._abort_reconnect()
else:
self._prepare_for_connect()
return
# We are connected
if show == 'offline':
self.set_user_activity(None)
self.set_user_mood(None)
self.set_user_tune(None)
self.set_user_location(None)
presence = self.get_module('Presence').get_presence(
typ='unavailable',
status=message,
caps=False)
self.send_stanza(presence)
self.disconnect(gracefully=True,
reconnect=False,
destroy_client=True)
return
self.update_presence()
def update_presence(self, include_muc=True):
status, message, idle = self.get_presence_state()
self._priority = app.get_priority(self._account, status)
self.get_module('Presence').send_presence(
priority=self._priority,
show=status,
status=message,
idle_time=idle)
if include_muc:
self.get_module('MUC').update_presence()
def set_user_activity(self, activity):
self.get_module('UserActivity').set_activity(activity)
def set_user_mood(self, mood):
self.get_module('UserMood').set_mood(mood)
def set_user_tune(self, tune):
self.get_module('UserTune').set_tune(tune)
def set_user_location(self, location):
self.get_module('UserLocation').set_location(location)
def get_module(self, name):
return modules.get(self._account, name)
@helpers.call_counter
def connect_machine(self):
log.info('Connect machine state: %s', self._connect_machine_calls)
if self._connect_machine_calls == 1:
self.get_module('MetaContacts').get_metacontacts()
elif self._connect_machine_calls == 2:
self.get_module('Delimiter').get_roster_delimiter()
elif self._connect_machine_calls == 3:
self.get_module('Roster').request_roster()
elif self._connect_machine_calls == 4:
self._finish_connect()
def _finish_connect(self):
self._status_sync_on_resume = False
self._set_client_available()
# We did not resume the stream, so we are not joined any MUCs
self.update_presence(include_muc=False)
self.get_module('Bookmarks').request_bookmarks()
self.get_module('SoftwareVersion').set_enabled(True)
self.get_module('Annotations').request_annotations()
self.get_module('Blocking').get_blocking_list()
# Inform GUI we just signed in
app.nec.push_incoming_event(NetworkEvent(
'signed-in', account=self._account, conn=self))
modules.send_stored_publish(self._account)
def send_stanza(self, stanza):
"""
Send a stanza untouched
"""
return self._client.send_stanza(stanza)
def send_message(self, message):
if not self._state.is_available:
log.warning('Trying to send message while offline')
return
stanza = self.get_module('Message').build_message_stanza(message)
message.stanza = stanza
if message.contact is None:
# Only Single Message should have no contact
self._send_message(message)
return
method = message.contact.settings.get('encryption')
if not method:
self._send_message(message)
return
# TODO: Make extension point return encrypted message
extension = 'encrypt'
if message.is_groupchat:
extension = 'gc_encrypt'
app.plugin_manager.extension_point(extension + method,
self,
message,
self._send_message)
def _send_message(self, message):
message.set_sent_timestamp()
message.message_id = self.send_stanza(message.stanza)
app.nec.push_incoming_event(
MessageSentEvent(None, jid=message.jid, **vars(message)))
if message.is_groupchat:
return
self.get_module('Message').log_message(message)
def send_messages(self, jids, message):
if not self._state.is_available:
log.warning('Trying to send message while offline')
return
for jid in jids:
message = message.copy()
message.contact = app.contacts.create_contact(jid, message.account)
stanza = self.get_module('Message').build_message_stanza(message)
message.stanza = stanza
self._send_message(message)
def _prepare_for_connect(self):
custom_host = get_custom_host(self._account)
if custom_host is not None:
self._client.set_custom_host(*custom_host)
gssapi = app.settings.get_account_setting(self._account,
'enable_gssapi')
if gssapi:
self._client.set_mechs(['GSSAPI'])
anonymous = app.settings.get_account_setting(self._account,
'anonymous_auth')
if anonymous:
self._client.set_mechs(['ANONYMOUS'])
if app.settings.get_account_setting(self._account,
'use_plain_connection'):
self._client.set_connection_types([ConnectionType.PLAIN])
proxy = get_user_proxy(self._account)
if proxy is not None:
self._client.set_proxy(proxy)
self.connect()
def connect(self, ignored_tls_errors=None):
if self._state not in (ClientState.DISCONNECTED,
ClientState.RECONNECT_SCHEDULED):
# Do not try to reco while we are already trying
return
log.info('Connect')
self._client.set_ignored_tls_errors(ignored_tls_errors)
self._reconnect = True
self._disable_reconnect_timer()
self._set_state(ClientState.CONNECTING)
if warn_about_plain_connection(self._account,
self._client.connection_types):
app.nec.push_incoming_event(NetworkEvent(
'plain-connection',
account=self._account,
connect=self._client.connect,
abort=self._abort_reconnect))
return
self._client.connect()
def _schedule_reconnect(self):
self._set_state(ClientState.RECONNECT_SCHEDULED)
log.info("Reconnect to %s in 3s", self._account)
self._reconnect_timer_source = GLib.timeout_add_seconds(
3, self._prepare_for_connect)
def _abort_reconnect(self):
self._set_state(ClientState.DISCONNECTED)
self._disable_reconnect_timer()
app.nec.push_incoming_event(
NetworkEvent('our-show', account=self._account, show='offline'))
if self._destroy_client:
self._client.destroy()
self._client = None
self._destroy_client = False
self._create_client()
def _disable_reconnect_timer(self):
if self._reconnect_timer_source is not None:
GLib.source_remove(self._reconnect_timer_source)
self._reconnect_timer_source = None
def _idle_state_changed(self, monitor):
state = monitor.state.value
if monitor.is_awake():
self._idle_status = state
self._idle_status_message = ''
self._update_status()
return
if not app.settings.get(f'auto{state}'):
return
if (state in ('away', 'xa') and self._status == 'online' or
state == 'xa' and self._idle_status == 'away'):
self._idle_status = state
self._idle_status_message = get_idle_status_message(
state, self._status_message)
self._update_status()
def _update_status(self):
if not self._idle_status_enabled:
return
self._status = self._idle_status
if self._state.is_available:
self.update_presence()
else:
self._status_sync_on_resume = True
def _idle_status_active(self):
if not Monitor.is_available():
return False
if not self._idle_status_enabled:
return False
return self._idle_status != 'online'
def get_presence_state(self):
if self._idle_status_active():
return self._idle_status, self._idle_status_message, True
return self._status, self._status_message, False
@staticmethod
def _screensaver_state_changed(application, _param):
active = application.get_property('screensaver-active')
Monitor.set_extended_away(active)
def cleanup(self):
self._destroyed = True
if Monitor.is_available():
Monitor.disconnect(self._idle_handler_id)
app.app.disconnect(self._screensaver_handler_id)
if self._client is not None:
# cleanup() is called before nbmxpp.Client has disconnected,
# when we disable the account. So we need to unregister
# handlers here.
# TODO: cleanup() should not be called before disconnect is finished
for handler in modules.get_handlers(self):
self._client.unregister_handler(handler)
modules.unregister_modules(self)
def quit(self, kill_core):
if kill_core and self._state in (ClientState.CONNECTING,
ClientState.CONNECTED,
ClientState.AVAILABLE):
self.disconnect(gracefully=True, reconnect=False)

643
gajim/common/config.py Normal file
View File

@ -0,0 +1,643 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2005 Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Alex Mauer <hawke AT hawkesnest.net>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
# Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 James Newton <redshodan AT gmail.com>
# Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
from typing import Tuple # pylint: disable=unused-import
import re
import copy
from enum import IntEnum, unique
from gi.repository import GLib
import gajim
from gajim.common.i18n import _
@unique
class Option(IntEnum):
TYPE = 0
VAL = 1
DESC = 2
# If Option.RESTART is True - we need restart to use our changed option
# Option.DESC also should be there
RESTART = 3
opt_int = ['integer', 0]
opt_str = ['string', 0]
opt_bool = ['boolean', 0]
opt_color = ['color', r'(#[0-9a-fA-F]{6})|rgb\(\d+,\d+,\d+\)|rgba\(\d+,\d+,\d+,[01]\.?\d*\)']
class Config:
DEFAULT_ICONSET = 'dcraven'
DEFAULT_MOOD_ICONSET = 'default'
DEFAULT_ACTIVITY_ICONSET = 'default'
__options = ({
# name: [ type, default_value, help_string, restart ]
'autopopup': [opt_bool, False],
'autopopupaway': [opt_bool, False],
'sounddnd': [opt_bool, False, _('Play sound even when being busy.')],
'showoffline': [opt_bool, True],
'show_only_chat_and_online': [opt_bool, False, _('Show only online and free for chat contacts in the contact list.')],
'show_transports_group': [opt_bool, True],
'autoaway': [opt_bool, True],
'autoawaytime': [opt_int, 5, _('Time in minutes, after which your status changes to away.')],
'autoaway_message': [opt_str, _('$S (Away: Idle more than $T min)'), _('$S will be replaced by current status message, $T by the \'autoawaytime\' value.')],
'autoxa': [opt_bool, True],
'autoxatime': [opt_int, 15, _('Time in minutes, after which your status changes to not available.')],
'autoxa_message': [opt_str, _('$S (Not available: Idle more than $T min)'), _('$S will be replaced by current status message, $T by the \'autoxatime\' value.')],
'ask_online_status': [opt_bool, False],
'ask_offline_status': [opt_bool, False],
'trayicon': [opt_str, 'always', _('When to show the notification area icon. Can be \'never\', \'on_event\', and \'always\'.'), False],
'allow_hide_roster': [opt_bool, False, _('Allow to hide the contact list window even if the notification area icon is not shown.'), False],
'iconset': [opt_str, DEFAULT_ICONSET, '', True],
'use_transports_iconsets': [opt_bool, True, '', True],
'collapsed_rows': [opt_str, '', _('List of rows (accounts and groups) that are collapsed (space separated).'), True],
'roster_theme': [opt_str, 'default', '', True],
'mergeaccounts': [opt_bool, False, '', True],
'sort_by_show_in_roster': [opt_bool, True, '', True],
'sort_by_show_in_muc': [opt_bool, False, '', True],
'use_speller': [opt_bool, False, ],
'show_xhtml': [opt_bool, True, ],
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'print_time': [opt_str, 'always', _('\'always\' - print time for every message.\n\'sometimes\' - print time every print_ichat_every_foo_minutes minute.\n\'never\' - never print time.')],
'emoticons_theme': [opt_str, 'noto-emoticons', '', True],
'ascii_formatting': [opt_bool, True,
_('Treat * / _ pairs as possible formatting characters.'), True],
'show_ascii_formatting_chars': [opt_bool, True, _('If enabled, do not '
'remove */_ . So *abc* will be bold but with * * not removed.')],
'sounds_on': [opt_bool, True],
'gc_refer_to_nick_char': [opt_str, ',', _('Character to add after nickname when using nickname completion (tab) in group chat.')],
'msgwin-max-state': [opt_bool, False],
'msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'msgwin-width': [opt_int, 500],
'msgwin-height': [opt_int, 440],
'chat-msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'chat-msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'chat-msgwin-width': [opt_int, 480],
'chat-msgwin-height': [opt_int, 440],
'gc-msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'gc-msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'gc-msgwin-width': [opt_int, 600],
'gc-msgwin-height': [opt_int, 440],
'pm-msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'pm-msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'pm-msgwin-width': [opt_int, 480],
'pm-msgwin-height': [opt_int, 440],
'single-msg-x-position': [opt_int, 0],
'single-msg-y-position': [opt_int, 0],
'single-msg-width': [opt_int, 400],
'single-msg-height': [opt_int, 280],
'save-roster-position': [opt_bool, True, _('If enabled, Gajim will save the contact list window position when hiding it, and restore it when showing the contact list window again.')],
'roster_x-position': [opt_int, 0],
'roster_y-position': [opt_int, 0],
'roster_width': [opt_int, 200],
'roster_height': [opt_int, 400],
'roster_hpaned_position': [opt_int, 200],
'roster_on_the_right': [opt_bool, False, _('Place the contact list on the right in single window mode'), True],
'history_window_width': [opt_int, -1],
'history_window_height': [opt_int, 450],
'history_window_x-position': [opt_int, 0],
'history_window_y-position': [opt_int, 0],
'latest_disco_addresses': [opt_str, ''],
'time_stamp': [opt_str, '%x | %X ', _('This option lets you customize the timestamp that is printed in conversation. For example \'[%H:%M] \' will show \'[hour:minute] \'. See python doc on strftime for full documentation (https://docs.python.org/3/library/time.html#time.strftime).')],
'before_nickname': [opt_str, '', _('Characters that are printed before the nickname in conversations.')],
'after_nickname': [opt_str, ':', _('Characters that are printed after the nickname in conversations.')],
'change_roster_title': [opt_bool, True, _('If enabled, Gajim will add * and [n] in contact list window title.')],
'restore_lines': [opt_int, 10, _('Number of messages from chat history to be restored when a chat tab/window is reopened.')],
'restore_timeout': [opt_int, -1, _('How far back in time (minutes) chat history is restored. -1 means no limit.')],
'send_on_ctrl_enter': [opt_bool, False, _('Send message on Ctrl+Enter and make a new line with Enter.')],
'last_roster_visible': [opt_bool, True],
'key_up_lines': [opt_int, 25, _('How many lines to store for Ctrl+KeyUP (previously sent messages).')],
'version': [opt_str, gajim.__version__], # which version created the config
'search_engine': [opt_str, 'https://duckduckgo.com/?q=%s'],
'dictionary_url': [opt_str, 'WIKTIONARY', _('Either a custom URL with %%s in it (where %%s is the word/phrase) or \'WIKTIONARY\' (which means use Wikitionary).')],
'always_english_wikipedia': [opt_bool, False],
'always_english_wiktionary': [opt_bool, True],
'remote_control': [opt_bool, False, _('If checked, Gajim can be controlled remotely using gajim-remote.'), True],
'print_ichat_every_foo_minutes': [opt_int, 5, _('When not printing time for every message (\'print_time\'==sometimes), print it every x minutes.')],
'confirm_paste_image': [opt_bool, True, _('Ask before pasting an image.')],
'confirm_close_muc': [opt_bool, True, _('Ask before closing a group chat tab/window.')],
'confirm_close_multiple_tabs': [opt_bool, True, _('Ask before closing tabbed chat window if there are chats that can lose data (chat, private chat, group chat that will not be minimized).')],
'notify_on_file_complete': [opt_bool, True],
'file_transfers_port': [opt_int, 28011],
'ft_add_hosts_to_send': [opt_str, '', _('List of send hosts (comma separated) in addition to local interfaces for file transfers (in case of address translation/port forwarding).')],
'use_kib_mib': [opt_bool, False, _('IEC standard says KiB = 1024 bytes, KB = 1000 bytes.')],
'notify_on_all_muc_messages': [opt_bool, False],
'trayicon_notification_on_events': [opt_bool, True, _('Notify of events in the notification area.')],
'last_save_dir': [opt_str, ''],
'last_send_dir': [opt_str, ''],
'last_sounds_dir': [opt_str, ''],
'tabs_position': [opt_str, 'left'],
'tabs_always_visible': [opt_bool, False, _('Show tab when only one conversation?')],
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')],
'notification_preview_message': [opt_bool, True, _('Preview new messages in notification popup?')],
'notification_position_x': [opt_int, -1],
'notification_position_y': [opt_int, -1],
'muc_highlight_words': [opt_str, '', _('A list of words (semicolon separated) that will be highlighted in group chats.')],
'quit_on_roster_x_button': [opt_bool, False, _('If enabled, Gajim quits when clicking the X button of your Window Manager. This setting is taken into account only if the notification area icon is used.')],
'hide_on_roster_x_button': [opt_bool, False, _('If enabled, Gajim hides the contact list window when pressing the X button instead of minimizing into the notification area.')],
'show_status_msgs_in_roster': [opt_bool, True, _('If enabled, Gajim will display the status message (if not empty) underneath the contact name in the contact list window.'), True],
'show_avatars_in_roster': [opt_bool, True, '', True],
'show_mood_in_roster': [opt_bool, True, '', True],
'show_activity_in_roster': [opt_bool, True, '', True],
'show_tunes_in_roster': [opt_bool, True, '', True],
'show_location_in_roster': [opt_bool, True, '', True],
'avatar_position_in_roster': [opt_str, 'right', _('Define the position of avatars in the contact list. Can be \'left\' or \'right\'.'), True],
'print_status_in_chats': [opt_bool, False, _('If disabled, Gajim will no longer print status messages in chats when a contact changes their status (and/or their status message).')],
'print_join_left_default': [opt_bool, False, _('Default Setting: Show a status message for every join or leave in a group chat.')],
'print_status_muc_default': [opt_bool, False, _('Default Setting: Show a status message for all status changes (away, dnd, etc.) of users in a group chat.')],
'log_contact_status_changes': [opt_bool, False],
'roster_window_skip_taskbar': [opt_bool, False, _('Don\'t show contact list window in the system taskbar.')],
'use_urgency_hint': [opt_bool, True, _('If enabled, Gajim makes the window flash (the default behaviour in most Window Managers) when holding pending events.')],
'notification_timeout': [opt_int, 5],
'one_message_window': [opt_str, 'always',
#always, never, peracct, pertype should not be translated
_('Controls the window where new messages are placed.\n\'always\' - All messages are sent to a single window.\n\'always_with_roster\' - Like \'always\' but the messages are in a single window along with the contact list.\n\'never\' - All messages get their own window.\n\'peracct\' - Messages for each account are sent to a specific window.\n\'pertype\' - Each message type (e.g. chats vs. group chats) is sent to a specific window.')],
'show_roster_on_startup':[opt_str, 'always', _('Show contact list window on startup.\n\'always\' - Always show contact list window.\n\'never\' - Never show contact list window.\n\'last_state\' - Restore last state of the contact list window.')],
'escape_key_closes': [opt_bool, False, _('If enabled, pressing Esc closes a tab/window.')],
'hide_groupchat_banner': [opt_bool, False, _('Hides the banner in a group chat window.')],
'hide_chat_banner': [opt_bool, False, _('Hides the banner in a 1:1 chat window.')],
'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the group chat participants list in a group chat window.')],
'chat_merge_consecutive_nickname': [opt_bool, False, _('In a chat, show the nickname at the beginning of a line only when it\'s not the same person talking as in the previous message.')],
'chat_merge_consecutive_nickname_indent': [opt_str, ' ', _('Indentation when using merge consecutive nickname.')],
'ctrl_tab_go_to_next_composing': [opt_bool, True, _('Ctrl+Tab switches to the next composing tab when there are no tabs with messages pending.')],
'confirm_metacontacts': [opt_str, '', _('Show a confirmation dialog to create metacontacts? Empty string means never show the dialog.')],
'confirm_block': [opt_str, '', _('Show a confirmation dialog to block a contact? Empty string means never show the dialog.')],
'enable_negative_priority': [opt_bool, False, _('If enabled, you will be able to set a negative priority to your account in the Accounts window. BE CAREFUL, when you are logged in with a negative priority, you will NOT receive any message from your server.')],
'show_contacts_number': [opt_bool, True, _('If enabled, Gajim will show both the number of online and total contacts in account rows as well as in group rows.')],
'scroll_roster_to_last_message': [opt_bool, True, _('If enabled, Gajim will scroll and select the contact who sent you the last message, if the chat window is not already opened.')],
'change_status_window_timeout': [opt_int, 15, _('Time of inactivity needed before the change status window closes down.')],
'max_conversation_lines': [opt_int, 500, _('Maximum number of lines that are printed in conversations. Oldest lines are cleared.')],
'uri_schemes': [opt_str, 'aaa:// aaas:// acap:// cap:// cid: crid:// data: dav: dict:// dns: fax: file:/ ftp:// geo: go: gopher:// h323: http:// https:// iax: icap:// im: imap:// info: ipp:// iris: iris.beep: iris.xpc: iris.xpcs: iris.lwz: ldap:// mid: modem: msrp:// msrps:// mtqp:// mupdate:// news: nfs:// nntp:// opaquelocktoken: pop:// pres: prospero:// rtsp:// service: sip: sips: sms: snmp:// soap.beep:// soap.beeps:// tag: tel: telnet:// tftp:// thismessage:/ tip:// tv: urn:// vemmi:// xmlrpc.beep:// xmlrpc.beeps:// z39.50r:// z39.50s:// about: apt: cvs:// daap:// ed2k:// feed: fish:// git:// iax2: irc:// ircs:// ldaps:// magnet: mms:// rsync:// ssh:// svn:// sftp:// smb:// webcal:// aesgcm://', _('Valid URI schemes. Only schemes in this list will be accepted as \'real\' URI (mailto and xmpp are handled separately).'), True],
'shell_like_completion': [opt_bool, False, _('If enabled, completion in group chats will be like a shell auto-completion.')],
'audio_input_device': [opt_str, 'autoaudiosrc ! volume name=gajim_vol'],
'audio_output_device': [opt_str, 'autoaudiosink'],
'video_input_device': [opt_str, 'autovideosrc'],
'video_framerate': [opt_str, '', _('Optionally fix Jingle output video framerate. Example: 10/1 or 25/2.')],
'video_size': [opt_str, '', _('Optionally resize Jingle output video. Example: 320x240.')],
'video_see_self': [opt_bool, True, _('If enabled, you will see your webcam\'s video stream as well.')],
'audio_input_volume': [opt_int, 50],
'audio_output_volume': [opt_int, 50],
'use_stun_server': [opt_bool, False, _('If enabled, Gajim will try to use a STUN server when using Jingle. The one in \'stun_server\' option, or the one given by the XMPP server.')],
'stun_server': [opt_str, '', _('STUN server to use when using Jingle')],
'global_proxy': [opt_str, '', _('Proxy used for all outgoing connections if the account does not have a specific proxy configured.')],
'ignore_incoming_attention': [opt_bool, False, _('If enabled, Gajim will ignore incoming attention requests (\'wizz\').')],
'remember_opened_chat_controls': [opt_bool, True, _('If enabled, Gajim will reopen chat windows that were opened last time Gajim was closed.')],
'positive_184_ack': [opt_bool, False, _('If enabled, Gajim will display an icon to show that sent messages have been received by your contact.')],
'use_keyring': [opt_bool, True, _('If enabled, Gajim will use the System\'s Keyring to store account passwords.')],
'remote_commands': [opt_bool, False, _('If enabled, Gajim will execute XEP-0146 Commands.')],
'dark_theme': [opt_int, 2, _('2: System, 1: Enabled, 0: Disabled')],
'public_room_sync_threshold': [opt_int, 1, _('Maximum history in days we request from a public group chat archive. 0: As much as possible.')],
'private_room_sync_threshold': [opt_int, 0, _('Maximum history in days we request from a private group chat archive. 0: As much as possible.')],
'show_subject_on_join': [opt_bool, True, _('If enabled, Gajim shows the group chat subject in the chat window when joining.')],
'show_chatstate_in_roster': [opt_bool, True, _('If enabled, the contact row is colored according to the current chat state of the contact.')],
'show_chatstate_in_tabs': [opt_bool, True, _('If enabled, the tab is colored according to the current chat state of the contact.')],
'show_chatstate_in_banner': [opt_bool, True, _('Shows a text in the banner that describes the current chat state of the contact.')],
'send_chatstate_default': [opt_str, 'composing_only', _('Chat state notifications that are sent to contacts. Possible values: all, composing_only, disabled')],
'send_chatstate_muc_default': [opt_str, 'composing_only', _('Chat state notifications that are sent to the group chat. Possible values: \'all\', \'composing_only\', \'disabled\'')],
'muclumbus_api_jid': [opt_str, 'api@search.jabber.network'],
'muclumbus_api_http_uri': [opt_str, 'https://search.jabber.network/api/1.0/search'],
'muclumbus_api_pref': [opt_str, 'http', _('API Preferences. Possible values: \'http\', \'iq\'')],
'command_system_execute': [opt_bool, False, _('If enabled, Gajim will execute commands (/show, /sh, /execute, /exec).')],
'groupchat_roster_width': [opt_int, 210, _('Width of group chat roster in pixel')],
'dev_force_bookmark_2': [opt_bool, False, _('Force Bookmark 2 usage')],
'show_help_start_chat': [opt_bool, True, _('Shows an info bar with helpful hints in the Start / Join Chat dialog')],
'check_for_update': [opt_bool, True, _('Check for Gajim updates periodically')],
'last_update_check': [opt_str, '', _('Date of the last update check')],
'always_ask_for_status_message': [opt_bool, False],
}, {}) # type: Tuple[Dict[str, List[Any]], Dict[Any, Any]]
__options_per_key = {
'accounts': ({
'name': [opt_str, '', '', True],
'account_label': [opt_str, '', '', False],
'account_color': [opt_color, 'rgb(85, 85, 85)'],
'hostname': [opt_str, '', '', True],
'anonymous_auth': [opt_bool, False],
'avatar_sha': [opt_str, '', '', False],
'client_cert': [opt_str, '', '', True],
'client_cert_encrypted': [opt_bool, False, '', False],
'savepass': [opt_bool, False],
'password': [opt_str, ''],
'resource': [opt_str, 'gajim.$rand', '', True],
'priority': [opt_int, 0, '', True],
'adjust_priority_with_status': [opt_bool, False, _('Priority will change automatically according to your status. Priorities are defined in \'autopriority_*\' options.')],
'autopriority_online': [opt_int, 50],
'autopriority_chat': [opt_int, 50],
'autopriority_away': [opt_int, 40],
'autopriority_xa': [opt_int, 30],
'autopriority_dnd': [opt_int, 20],
'autoconnect': [opt_bool, False, '', True],
'restore_last_status': [opt_bool, False, _('If enabled, the last status will be restored.')],
'autoauth': [opt_bool, False, _('If enabled, contacts requesting authorization will be accepted automatically.')],
'active': [opt_bool, True, _('If disabled, this account will be disabled and will not appear in the contact list window.'), True],
'proxy': [opt_str, '', '', True],
'keyid': [opt_str, '', '', True],
'keyname': [opt_str, '', '', True],
'use_plain_connection': [opt_bool, False, _('Use an unencrypted connection to the server')],
'confirm_unencrypted_connection': [opt_bool, True],
'use_custom_host': [opt_bool, False, '', True],
'custom_port': [opt_int, 5222, '', True],
'custom_host': [opt_str, '', '', True],
'custom_type': [opt_str, 'START TLS', _('ConnectionType: START TLS, DIRECT TLS or PLAIN'), True],
'sync_with_global_status': [opt_bool, False, ],
'no_log_for': [opt_str, '', _('List of XMPP Addresses (space separated) for which you do not want to store chat history. You can also add the name of an account to disable storing chat history for this account.')],
'attached_gpg_keys': [opt_str, ''],
'http_auth': [opt_str, 'ask'], # yes, no, ask
# proxy65 for FT
'file_transfer_proxies': [opt_str, ''],
'use_ft_proxies': [opt_bool, False, _('If enabled, Gajim will use your IP and proxies defined in \'file_transfer_proxies\' option for file transfers.'), True],
'test_ft_proxies_on_startup': [opt_bool, False, _('If enabled, Gajim will test file transfer proxies on startup to be sure they work. Openfire\'s proxies are known to fail this test even if they work.')],
'msgwin-x-position': [opt_int, -1], # Default is to let the wm decide
'msgwin-y-position': [opt_int, -1], # Default is to let the wm decide
'msgwin-width': [opt_int, 480],
'msgwin-height': [opt_int, 440],
'is_zeroconf': [opt_bool, False],
'last_status': [opt_str, 'online'],
'last_status_msg': [opt_str, ''],
'zeroconf_first_name': [opt_str, '', '', True],
'zeroconf_last_name': [opt_str, '', '', True],
'zeroconf_jabber_id': [opt_str, '', '', True],
'zeroconf_email': [opt_str, '', '', True],
'answer_receipts': [opt_bool, True, _('If enabled, Gajim will answer to message receipt requests.')],
'publish_tune': [opt_bool, False],
'publish_location': [opt_bool, False],
'request_user_data': [opt_bool, True],
'ignore_unknown_contacts': [opt_bool, False],
'send_os_info': [opt_bool, True, _('Allow Gajim to send information about the operating system you are running.')],
'send_time_info': [opt_bool, True, _('Allow Gajim to send your local time.')],
'send_idle_time': [opt_bool, True],
'roster_version': [opt_str, ''],
'subscription_request_msg': [opt_str, '', _('Message that is sent to contacts you want to add.')],
'ft_send_local_ips': [opt_bool, True, _('If enabled, Gajim will send your local IP so your contact can connect to your machine for file transfers.')],
'opened_chat_controls': [opt_str, '', _('List of XMPP Addresses (space separated) for which the chat window will be re-opened on next startup.')],
'recent_groupchats': [opt_str, ''],
'filetransfer_preference' : [opt_str, 'httpupload', _('Preferred file transfer mechanism for file drag&drop on a chat window. Can be \'httpupload\' (default) or \'jingle\'.')],
'allow_posh': [opt_bool, True, _('Allow certificate verification with POSH.')],
}, {}),
'statusmsg': ({
'message': [opt_str, ''],
'activity': [opt_str, ''],
'subactivity': [opt_str, ''],
'activity_text': [opt_str, ''],
'mood': [opt_str, ''],
'mood_text': [opt_str, ''],
}, {}),
'soundevents': ({
'enabled': [opt_bool, True],
'path': [opt_str, ''],
}, {}),
'proxies': ({
'type': [opt_str, 'http'],
'host': [opt_str, ''],
'port': [opt_int, 3128],
'useauth': [opt_bool, False],
'user': [opt_str, ''],
'pass': [opt_str, ''],
}, {}),
'contacts': ({
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to contacts. Possible values: \'all\', \'composing_only\', \'disabled\'')],
}, {}),
'encryption': ({
'encryption': [opt_str, '', _('The currently active encryption for that contact.')],
}, {}),
'rooms': ({
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'notify_on_all_messages': [opt_bool, False, _('If enabled, a notification is created for every message in this group chat.')],
'print_status': [opt_bool, False, _('Show a status message for all status changes (away, dnd, etc.) of users in a group chat.')],
'print_join_left': [opt_bool, False, _('Show a status message for every join or leave in a group chat.')],
'minimize_on_autojoin': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when joining automatically.')],
'minimize_on_close': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when closing it.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to the group chat. Possible values: \'all\', \'composing_only\' or \'disabled\'.')],
}, {}),
'plugins': ({
'active': [opt_bool, False, _('If enabled, plugins will be activated on startup (this is saved when exiting Gajim). This option SHOULD NOT be used to (de)activate plugins. Use the plugin window instead.')],
}, {}),
} # type: Dict[str, Tuple[Dict[str, List[Any]], Dict[Any, Any]]]
statusmsg_default = {
_('Sleeping'): ['ZZZZzzzzzZZZZZ', 'inactive', 'sleeping', '', 'sleepy', ''],
_('Back soon'): [_('Back in some minutes.'), '', '', '', '', ''],
_('Eating'): [_('I\'m eating.'), 'eating', 'other', '', '', ''],
_('Movie'): [_('I\'m watching a movie.'), 'relaxing', 'watching_a_movie', '', '', ''],
_('Working'): [_('I\'m working.'), 'working', 'other', '', '', ''],
_('Phone'): [_('I\'m on the phone.'), 'talking', 'on_the_phone', '', '', ''],
_('Out'): [_('I\'m out enjoying life.'), 'relaxing', 'going_out', '', '', ''],
}
soundevents_default = {
'attention_received': [True, 'attention.wav'],
'first_message_received': [True, 'message1.wav'],
'next_message_received_focused': [True, 'message2.wav'],
'next_message_received_unfocused': [True, 'message2.wav'],
'contact_connected': [False, 'connected.wav'],
'contact_disconnected': [False, 'disconnected.wav'],
'message_sent': [False, 'sent.wav'],
'muc_message_highlight': [True, 'gc_message1.wav', _('Sound to play when a group chat message contains one of the words in \'muc_highlight_words\' or your nickname is mentioned.')],
'muc_message_received': [True, 'gc_message2.wav', _('Sound to play when any group chat message arrives.')],
}
proxies_default = {
_('Tor'): ['socks5', 'localhost', 9050],
}
def foreach(self, cb, data=None):
for opt in self.__options[1]:
cb(data, opt, None, self.__options[1][opt])
for opt in self.__options_per_key:
cb(data, opt, None, None)
dict_ = self.__options_per_key[opt][1]
for opt2 in dict_.keys():
cb(data, opt2, [opt], None)
for opt3 in dict_[opt2]:
cb(data, opt3, [opt, opt2], dict_[opt2][opt3])
def get_children(self, node=None):
"""
Tree-like interface
"""
if node is None:
for child, option in self.__options[1].items():
yield (child, ), option
for grandparent in self.__options_per_key:
yield (grandparent, ), None
elif len(node) == 1:
grandparent, = node
for parent in self.__options_per_key[grandparent][1]:
yield (grandparent, parent), None
elif len(node) == 2:
grandparent, parent = node
children = self.__options_per_key[grandparent][1][parent]
for child, option in children.items():
yield (grandparent, parent, child), option
else:
raise ValueError('Invalid node')
def is_valid_int(self, val):
try:
ival = int(val)
except Exception:
return None
return ival
def is_valid_bool(self, val):
if val == 'True':
return True
if val == 'False':
return False
ival = self.is_valid_int(val)
if ival:
return True
if ival is None:
return None
return False
def is_valid_string(self, val):
return val
def is_valid(self, type_, val):
if not type_:
return None
if type_[0] == 'boolean':
return self.is_valid_bool(val)
if type_[0] == 'integer':
return self.is_valid_int(val)
if type_[0] == 'string':
return self.is_valid_string(val)
if re.match(type_[1], val):
return val
return None
def set(self, optname, value):
if optname not in self.__options[1]:
return
value = self.is_valid(self.__options[0][optname][Option.TYPE], value)
if value is None:
return
self.__options[1][optname] = value
self._timeout_save()
def get(self, optname=None):
if not optname:
return list(self.__options[1].keys())
if optname not in self.__options[1]:
return None
return self.__options[1][optname]
def get_default(self, optname):
if optname not in self.__options[0]:
return None
return self.__options[0][optname][Option.VAL]
def get_type(self, optname):
if optname not in self.__options[0]:
return None
return self.__options[0][optname][Option.TYPE][0]
def get_desc(self, optname):
if optname not in self.__options[0]:
return None
if len(self.__options[0][optname]) > Option.DESC:
return self.__options[0][optname][Option.DESC]
def get_restart(self, optname):
if optname not in self.__options[0]:
return None
if len(self.__options[0][optname]) > Option.RESTART:
return self.__options[0][optname][Option.RESTART]
def add_per(self, typename, name): # per_group_of_option
if typename not in self.__options_per_key:
return
opt = self.__options_per_key[typename]
if name in opt[1]:
# we already have added group name before
return 'you already have added %s before' % name
opt[1][name] = {}
for o in opt[0]:
opt[1][name][o] = opt[0][o][Option.VAL]
self._timeout_save()
def del_per(self, typename, name, subname=None): # per_group_of_option
if typename not in self.__options_per_key:
return
opt = self.__options_per_key[typename]
if subname is None:
del opt[1][name]
# if subname is specified, delete the item in the group.
elif subname in opt[1][name]:
del opt[1][name][subname]
self._timeout_save()
def del_all_per(self, typename, subname):
# Deletes all settings per typename
# Example: Delete `account_label` for all accounts
if typename not in self.__options_per_key:
raise ValueError('typename %s does not exist' % typename)
opt = self.__options_per_key[typename]
for name in opt[1]:
try:
del opt[1][name][subname]
except KeyError:
pass
self._timeout_save()
def set_per(self, optname, key, subname, value): # per_group_of_option
if optname not in self.__options_per_key:
return
if not key:
return
dict_ = self.__options_per_key[optname][1]
if key not in dict_:
self.add_per(optname, key)
obj = dict_[key]
if subname not in obj:
return
typ = self.__options_per_key[optname][0][subname][Option.TYPE]
value = self.is_valid(typ, value)
if value is None:
return
obj[subname] = value
self._timeout_save()
def get_per(self, optname, key=None, subname=None, default=None): # per_group_of_option
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][1]
if not key:
return list(dict_.keys())
if key not in dict_:
if default is not None:
return default
if subname in self.__options_per_key[optname][0]:
return self.__options_per_key[optname][0][subname][1]
return None
obj = dict_[key]
if not subname:
return obj
if subname not in obj:
return None
return obj[subname]
def get_all(self):
return copy.deepcopy(self.__options[1])
def get_all_per(self, optname):
return copy.deepcopy(self.__options_per_key[optname][1])
def get_default_per(self, optname, subname):
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][0]
if subname not in dict_:
return None
return dict_[subname][Option.VAL]
def get_type_per(self, optname, subname):
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][0]
if subname not in dict_:
return None
return dict_[subname][Option.TYPE][0]
def get_desc_per(self, optname, subname=None):
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][0]
if subname not in dict_:
return None
obj = dict_[subname]
if len(obj) > Option.DESC:
return obj[Option.DESC]
return None
def get_restart_per(self, optname, key=None, subname=None):
if optname not in self.__options_per_key:
return False
dict_ = self.__options_per_key[optname][0]
if not key:
return False
if key not in dict_:
return False
obj = dict_[key]
if not subname:
return False
if subname not in obj:
return False
if len(obj[subname]) > Option.RESTART:
return obj[subname][Option.RESTART]
return False
def get_options(self, optname, return_type=str):
options = self.get(optname).split(',')
options = [return_type(option.strip()) for option in options]
return options
def _init_options(self):
for opt in self.__options[0]:
self.__options[1][opt] = self.__options[0][opt][Option.VAL]
if gajim.IS_PORTABLE:
self.__options[1]['use_keyring'] = False
def _really_save(self):
from gajim.common import app
if app.interface:
app.interface.save_config()
self.save_timeout_id = None
return False
def _timeout_save(self):
if self.save_timeout_id:
return
self.save_timeout_id = GLib.timeout_add(1000, self._really_save)
def __init__(self):
#init default values
self._init_options()
self.save_timeout_id = None
for event in self.soundevents_default:
default = self.soundevents_default[event]
self.add_per('soundevents', event)
self.set_per('soundevents', event, 'enabled', default[0])
self.set_per('soundevents', event, 'path', default[1])

234
gajim/common/configpaths.py Normal file
View File

@ -0,0 +1,234 @@
# Copyright (C) 2006 Jean-Marie Traissard <jim AT lapin.org>
# Junglecow J <junglecow AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Brendan Taylor <whateley AT gmail.com>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Dict # pylint: disable=unused-import
from typing import List
from typing import Generator
from typing import Optional # pylint: disable=unused-import
from typing import Tuple
import os
import sys
import tempfile
from pathlib import Path
from gi.repository import GLib
import gajim
from gajim.common.i18n import _
from gajim.common.const import PathType, PathLocation
from gajim.common.types import PathTuple
def get(key: str) -> Path:
return _paths[key]
def get_plugin_dirs() -> List[Path]:
if gajim.IS_FLATPAK:
return [Path(_paths['PLUGINS_BASE']),
Path('/app/plugins')]
return [Path(_paths['PLUGINS_BASE']),
Path(_paths['PLUGINS_USER'])]
def get_paths(type_: PathType) -> Generator[Path, None, None]:
for key, value in _paths.items():
path_type = value[2]
if type_ != path_type:
continue
yield _paths[key]
def override_path(*args, **kwargs):
_paths.add(*args, **kwargs)
def set_separation(active: bool) -> None:
_paths.profile_separation = active
def set_profile(profile: str) -> None:
_paths.profile = profile
def set_config_root(config_root: str) -> None:
_paths.custom_config_root = Path(config_root).resolve()
def init() -> None:
_paths.init()
def create_paths() -> None:
for path in get_paths(PathType.FOLDER):
if path.is_file():
print(_('%s is a file but it should be a directory') % path)
print(_('Gajim will now exit'))
sys.exit()
if not path.exists():
for parent_path in reversed(path.parents):
# Create all parent folders
# don't use mkdir(parent=True), as it ignores `mode`
# when creating the parents
if not parent_path.exists():
print(('creating %s directory') % parent_path)
parent_path.mkdir(mode=0o700)
print(('creating %s directory') % path)
path.mkdir(mode=0o700)
class ConfigPaths:
def __init__(self) -> None:
self._paths = {} # type: Dict[str, PathTuple]
self.profile = ''
self.profile_separation = False
self.custom_config_root = None # type: Optional[Path]
if os.name == 'nt':
if gajim.IS_PORTABLE:
application_path = Path(sys.executable).parent
self.config_root = self.cache_root = self.data_root = \
application_path.parent / 'UserData'
else:
# Documents and Settings\[User Name]\Application Data\Gajim
self.config_root = self.cache_root = self.data_root = \
Path(os.environ['appdata']) / 'Gajim'
else:
self.config_root = Path(GLib.get_user_config_dir()) / 'gajim'
self.cache_root = Path(GLib.get_user_cache_dir()) / 'gajim'
self.data_root = Path(GLib.get_user_data_dir()) / 'gajim'
if sys.version_info < (3, 9):
import pkg_resources
basedir = Path(pkg_resources.resource_filename("gajim", "."))
else:
import importlib.resources
basedir = importlib.resources.files('gajim')
source_paths = [
('DATA', basedir / 'data'),
('STYLE', basedir / 'data' / 'style'),
('EMOTICONS', basedir / 'data' / 'emoticons'),
('GUI', basedir / 'data' / 'gui'),
('ICONS', basedir / 'data' / 'icons'),
('HOME', Path.home()),
('PLUGINS_BASE', basedir / 'data' / 'plugins'),
]
for path in source_paths:
self.add(*path)
def __getitem__(self, key: str) -> Path:
location, path, _ = self._paths[key]
if location == PathLocation.CONFIG:
return self.config_root / path
if location == PathLocation.CACHE:
return self.cache_root / path
if location == PathLocation.DATA:
return self.data_root / path
return path
def items(self) -> Generator[Tuple[str, PathTuple], None, None]:
for key, value in self._paths.items():
yield (key, value)
def _prepare(self, path: Path, unique: bool) -> Path:
if os.name == 'nt':
path = Path(str(path).capitalize())
if self.profile:
if unique or self.profile_separation:
return Path(f'{path}.{self.profile}')
return path
def add(self,
name: str,
path: Path,
location: PathLocation = None,
path_type: PathType = None,
unique: bool = False) -> None:
if location is not None:
path = self._prepare(path, unique)
self._paths[name] = (location, path, path_type)
def init(self):
if self.custom_config_root:
self.config_root = self.custom_config_root
self.cache_root = self.data_root = self.custom_config_root
user_dir_paths = [
('TMP', Path(tempfile.gettempdir())),
('MY_CONFIG', Path(), PathLocation.CONFIG, PathType.FOLDER),
('MY_CACHE', Path(), PathLocation.CACHE, PathType.FOLDER),
('MY_DATA', Path(), PathLocation.DATA, PathType.FOLDER),
]
for path in user_dir_paths:
self.add(*path)
# These paths are unique per profile
unique_profile_paths = [
# Data paths
('SECRETS_FILE', 'secrets', PathLocation.DATA, PathType.FILE),
('MY_PEER_CERTS', 'certs', PathLocation.DATA, PathType.FOLDER),
('CERT_STORE', 'cert_store', PathLocation.DATA, PathType.FOLDER),
('DEBUG', 'debug', PathLocation.DATA, PathType.FOLDER),
('PLUGINS_DATA', 'plugins_data', PathLocation.DATA, PathType.FOLDER),
# Config paths
('SETTINGS', 'settings.sqlite', PathLocation.CONFIG, PathType.FILE),
('CONFIG_FILE', 'config', PathLocation.CONFIG, PathType.FILE),
('PLUGINS_CONFIG_DIR',
'pluginsconfig', PathLocation.CONFIG, PathType.FOLDER),
('MY_CERT', 'localcerts', PathLocation.CONFIG, PathType.FOLDER),
]
for path in unique_profile_paths:
self.add(*path, unique=True)
# These paths are only unique per profile if the commandline arg
# `separate` is passed
paths = [
# Data paths
('LOG_DB', 'logs.db', PathLocation.DATA, PathType.FILE),
('PLUGINS_DOWNLOAD', 'plugins_download', PathLocation.CACHE, PathType.FOLDER),
('PLUGINS_USER', 'plugins', PathLocation.DATA, PathType.FOLDER),
('MY_EMOTS',
'emoticons', PathLocation.DATA, PathType.FOLDER_OPTIONAL),
('MY_ICONSETS',
'iconsets', PathLocation.DATA, PathType.FOLDER_OPTIONAL),
# Cache paths
('CACHE_DB', 'cache.db', PathLocation.CACHE, PathType.FILE),
('AVATAR', 'avatars', PathLocation.CACHE, PathType.FOLDER),
('BOB', 'bob', PathLocation.CACHE, PathType.FOLDER),
# Config paths
('MY_THEME', 'theme', PathLocation.CONFIG, PathType.FOLDER),
]
for path in paths:
self.add(*path)
_paths = ConfigPaths()

201
gajim/common/connection.py Normal file
View File

@ -0,0 +1,201 @@
# Copyright (C) 2003-2005 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
# Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
# Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org>
# Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import time
import logging
from gajim.common import helpers
from gajim.common import app
from gajim.common import idle
from gajim.common import modules
from gajim.common.nec import NetworkEvent
from gajim.common.const import ClientState
log = logging.getLogger('gajim.c.connection')
SERVICE_START_TLS = 'xmpp-client'
SERVICE_DIRECT_TLS = 'xmpps-client'
class CommonConnection:
"""
Common connection class, can be derived for normal connection or zeroconf
connection
"""
def __init__(self, name):
self.name = name
self._modules = {}
self.connection = None # xmpppy ClientCommon instance
self.is_zeroconf = False
self.password = None
self.server_resource = helpers.get_resource(self.name)
self.priority = app.get_priority(name, 'offline')
self.time_to_reconnect = None
self._reconnect_timer_source = None
self._state = ClientState.DISCONNECTED
self._status = 'offline'
self._status_message = ''
# If handlers have been registered
self.handlers_registered = False
self.pep = {}
self.roster_supported = True
self._stun_servers = [] # STUN servers of our jabber server
# Tracks the calls of the connect_machine() method
self._connect_machine_calls = 0
self.get_config_values_or_default()
def _set_state(self, state):
log.info('State: %s', state)
self._state = state
@property
def state(self):
return self._state
@property
def status(self):
return self._status
@property
def status_message(self):
return self._status_message
def _register_new_handlers(self, con):
for handler in modules.get_handlers(self):
if len(handler) == 5:
name, func, typ, ns, priority = handler
con.RegisterHandler(name, func, typ, ns, priority=priority)
else:
con.RegisterHandler(*handler)
self.handlers_registered = True
def _unregister_new_handlers(self, con):
if not con:
return
for handler in modules.get_handlers(self):
if len(handler) > 4:
handler = handler[:4]
con.UnregisterHandler(*handler)
self.handlers_registered = False
def dispatch(self, event, data):
"""
Always passes account name as first param
"""
app.ged.raise_event(event, self.name, data)
def get_module(self, name):
return modules.get(self.name, name)
def reconnect(self):
"""
To be implemented by derived classes
"""
raise NotImplementedError
def quit(self, kill_core):
if kill_core and app.account_is_connected(self.name):
self.disconnect(reconnect=False)
def new_account(self, name, config, sync=False):
"""
To be implemented by derived classes
"""
raise NotImplementedError
def _on_new_account(self, con=None, con_type=None):
"""
To be implemented by derived classes
"""
raise NotImplementedError
def _event_dispatcher(self, realm, event, data):
if realm == '':
if event == 'STANZA RECEIVED':
app.nec.push_incoming_event(
NetworkEvent('stanza-received',
conn=self,
stanza_str=str(data)))
elif event == 'DATA SENT':
app.nec.push_incoming_event(
NetworkEvent('stanza-sent',
conn=self,
stanza_str=str(data)))
def change_status(self, show, msg, auto=False):
if not msg:
msg = ''
self._status = show
self._status_message = msg
if self._state.is_disconnected:
if show == 'offline':
return
self.server_resource = helpers.get_resource(self.name)
self.connect_and_init(show, msg)
return
if self._state.is_connecting or self._state.is_reconnect_scheduled:
if show == 'offline':
self.disconnect(reconnect=False)
elif self._state.is_reconnect_scheduled:
self.reconnect()
return
# We are connected
if show == 'offline':
presence = self.get_module('Presence').get_presence(
typ='unavailable',
status=msg,
caps=False)
self.connection.send(presence, now=True)
self.disconnect(reconnect=False)
return
idle_time = None
if auto:
if app.is_installed('IDLE') and app.settings.get('autoaway'):
idle_sec = idle.Monitor.get_idle_sec()
idle_time = time.strftime(
'%Y-%m-%dT%H:%M:%SZ',
time.gmtime(time.time() - idle_sec))
self._update_status(show, msg, idle_time=idle_time)

View File

@ -0,0 +1,181 @@
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Junglecow J <junglecow AT gmail.com>
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Jean-Marie Traissard <jim AT lapin.org>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import operator
import nbxmpp
from gajim.common import app
from gajim.common.connection_handlers_events import PresenceReceivedEvent
from gajim.common.connection_handlers_events import NotificationEvent
log = logging.getLogger('gajim.c.connection_handlers')
# basic connection handlers used here and in zeroconf
class ConnectionHandlersBase:
def __init__(self):
# keep track of sessions this connection has with other JIDs
self.sessions = {}
def get_sessions(self, jid):
"""
Get all sessions for the given full jid
"""
if not app.interface.is_pm_contact(jid, self.name):
jid = app.get_jid_without_resource(jid)
try:
return list(self.sessions[jid].values())
except KeyError:
return []
def get_or_create_session(self, fjid, thread_id):
"""
Return an existing session between this connection and 'jid', returns a
new one if none exist
"""
pm = True
jid = fjid
if not app.interface.is_pm_contact(fjid, self.name):
pm = False
jid = app.get_jid_without_resource(fjid)
session = self.find_session(jid, thread_id)
if session:
return session
if pm:
return self.make_new_session(fjid, thread_id, type_='pm')
return self.make_new_session(fjid, thread_id)
def find_session(self, jid, thread_id):
try:
if not thread_id:
return self.find_null_session(jid)
return self.sessions[jid][thread_id]
except KeyError:
return None
def terminate_sessions(self):
self.sessions = {}
def delete_session(self, jid, thread_id):
if not jid in self.sessions:
jid = app.get_jid_without_resource(jid)
if not jid in self.sessions:
return
del self.sessions[jid][thread_id]
if not self.sessions[jid]:
del self.sessions[jid]
def find_null_session(self, jid):
"""
Find all of the sessions between us and a remote jid in which we haven't
received a thread_id yet and returns the session that we last sent a
message to
"""
sessions = list(self.sessions[jid].values())
# sessions that we haven't received a thread ID in
idless = [s for s in sessions if not s.received_thread_id]
# filter out everything except the default session type
chat_sessions = [s for s in idless if isinstance(s,
app.default_session_type)]
if chat_sessions:
# return the session that we last sent a message in
return sorted(chat_sessions,
key=operator.attrgetter('last_send'))[-1]
return None
def get_latest_session(self, jid):
"""
Get the session that we last sent a message to
"""
if jid not in self.sessions:
return None
sessions = self.sessions[jid].values()
if not sessions:
return None
return sorted(sessions, key=operator.attrgetter('last_send'))[-1]
def find_controlless_session(self, jid, resource=None):
"""
Find an active session that doesn't have a control attached
"""
try:
sessions = list(self.sessions[jid].values())
# filter out everything except the default session type
chat_sessions = [s for s in sessions if isinstance(s,
app.default_session_type)]
orphaned = [s for s in chat_sessions if not s.control]
if resource:
orphaned = [s for s in orphaned if s.resource == resource]
return orphaned[0]
except (KeyError, IndexError):
return None
def make_new_session(self, jid, thread_id=None, type_='chat', cls=None):
"""
Create and register a new session
thread_id=None to generate one.
type_ should be 'chat' or 'pm'.
"""
if not cls:
cls = app.default_session_type
sess = cls(self, nbxmpp.JID.from_string(jid), thread_id, type_)
# determine if this session is a pm session
# if not, discard the resource so that all sessions are stored bare
if type_ != 'pm':
jid = app.get_jid_without_resource(jid)
if not jid in self.sessions:
self.sessions[jid] = {}
self.sessions[jid][sess.thread_id] = sess
return sess
class ConnectionHandlers(ConnectionHandlersBase):
def __init__(self):
ConnectionHandlersBase.__init__(self)
app.nec.register_incoming_event(PresenceReceivedEvent)
app.nec.register_incoming_event(NotificationEvent)

View File

@ -0,0 +1,435 @@
# Copyright (C) 2010-2014 Yann Leboulanger <asterix AT lagaule.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=no-init
# pylint: disable=attribute-defined-outside-init
import logging
from nbxmpp.namespaces import Namespace
from gajim.common import nec
from gajim.common import helpers
from gajim.common import app
from gajim.common import i18n
from gajim.common.i18n import _
from gajim.common.jingle_transport import JingleTransportSocks5
from gajim.common.file_props import FilesProp
log = logging.getLogger('gajim.c.connection_handlers_events')
class PresenceReceivedEvent(nec.NetworkIncomingEvent):
name = 'presence-received'
class OurShowEvent(nec.NetworkIncomingEvent):
name = 'our-show'
def init(self):
self.reconnect = False
class MessageSentEvent(nec.NetworkIncomingEvent):
name = 'message-sent'
class ConnectionLostEvent(nec.NetworkIncomingEvent):
name = 'connection-lost'
def generate(self):
app.nec.push_incoming_event(OurShowEvent(None, conn=self.conn,
show='offline'))
return True
class FileRequestReceivedEvent(nec.NetworkIncomingEvent):
name = 'file-request-received'
def init(self):
self.jingle_content = None
self.FT_content = None
def generate(self):
self.id_ = self.stanza.getID()
self.fjid = self.conn.get_module('Bytestream')._ft_get_from(self.stanza)
self.jid = app.get_jid_without_resource(self.fjid)
if not self.jingle_content:
return
secu = self.jingle_content.getTag('security')
self.FT_content.use_security = bool(secu)
if secu:
fingerprint = secu.getTag('fingerprint')
if fingerprint:
self.FT_content.x509_fingerprint = fingerprint.getData()
if not self.FT_content.transport:
self.FT_content.transport = JingleTransportSocks5()
self.FT_content.transport.set_our_jid(
self.FT_content.session.ourjid)
self.FT_content.transport.set_connection(
self.FT_content.session.connection)
sid = self.stanza.getTag('jingle').getAttr('sid')
self.file_props = FilesProp.getNewFileProp(self.conn.name, sid)
self.file_props.transport_sid = self.FT_content.transport.sid
self.FT_content.file_props = self.file_props
self.FT_content.transport.set_file_props(self.file_props)
self.file_props.streamhosts.extend(
self.FT_content.transport.remote_candidates)
for host in self.file_props.streamhosts:
host['initiator'] = self.FT_content.session.initiator
host['target'] = self.FT_content.session.responder
self.file_props.session_type = 'jingle'
self.file_props.stream_methods = Namespace.BYTESTREAM
desc = self.jingle_content.getTag('description')
if self.jingle_content.getAttr('creator') == 'initiator':
file_tag = desc.getTag('file')
self.file_props.sender = self.fjid
self.file_props.receiver = self.conn.get_own_jid()
else:
file_tag = desc.getTag('file')
h = file_tag.getTag('hash')
h = h.getData() if h else None
n = file_tag.getTag('name')
n = n.getData() if n else None
pjid = app.get_jid_without_resource(self.fjid)
file_info = self.conn.get_module('Jingle').get_file_info(
pjid, hash_=h, name=n, account=self.conn.name)
self.file_props.file_name = file_info['file-name']
self.file_props.sender = self.conn.get_own_jid()
self.file_props.receiver = self.fjid
self.file_props.type_ = 's'
for child in file_tag.getChildren():
name = child.getName()
val = child.getData()
if val is None:
continue
if name == 'name':
self.file_props.name = val
if name == 'size':
self.file_props.size = int(val)
if name == 'hash':
self.file_props.algo = child.getAttr('algo')
self.file_props.hash_ = val
if name == 'date':
self.file_props.date = val
self.file_props.request_id = self.id_
file_desc_tag = file_tag.getTag('desc')
if file_desc_tag is not None:
self.file_props.desc = file_desc_tag.getData()
self.file_props.transfered_size = []
return True
class NotificationEvent(nec.NetworkIncomingEvent):
name = 'notification'
base_network_events = ['decrypted-message-received',
'gc-message-received',
'presence-received']
def generate(self):
# what's needed to compute output
self.account = self.base_event.conn.name
self.conn = self.base_event.conn
self.jid = ''
self.control = None
self.control_focused = False
self.first_unread = False
# For output
self.do_sound = False
self.sound_file = ''
self.sound_event = '' # gajim sound played if not sound_file is set
self.show_popup = False
self.do_popup = False
self.popup_title = ''
self.popup_text = ''
self.popup_event_type = ''
self.popup_msg_type = ''
self.icon_name = None
self.transport_name = None
self.show = None
self.popup_timeout = -1
self.do_command = False
self.command = ''
self.show_in_notification_area = False
self.show_in_roster = False
self.detect_type()
if self.notif_type == 'msg':
self.handle_incoming_msg_event(self.base_event)
elif self.notif_type == 'gc-msg':
self.handle_incoming_gc_msg_event(self.base_event)
elif self.notif_type == 'pres':
self.handle_incoming_pres_event(self.base_event)
return True
def detect_type(self):
if self.base_event.name == 'decrypted-message-received':
self.notif_type = 'msg'
if self.base_event.name == 'gc-message-received':
self.notif_type = 'gc-msg'
if self.base_event.name == 'presence-received':
self.notif_type = 'pres'
def handle_incoming_msg_event(self, msg_obj):
# don't alert for carbon copied messages from ourselves
if msg_obj.properties.is_sent_carbon:
return
if not msg_obj.msgtxt:
return
self.jid = msg_obj.jid
if msg_obj.properties.is_muc_pm:
self.jid = msg_obj.fjid
self.control = app.interface.msg_win_mgr.search_control(
msg_obj.jid, self.account, msg_obj.resource)
if self.control is None:
event_type = msg_obj.properties.type.value
if msg_obj.properties.is_muc_pm:
event_type = 'pm'
if len(app.events.get_events(
self.account, msg_obj.jid, [event_type])) <= 1:
self.first_unread = True
else:
self.control_focused = self.control.has_focus()
if msg_obj.properties.is_muc_pm:
nick = msg_obj.resource
else:
nick = app.get_name_from_jid(self.conn.name, self.jid)
if self.first_unread:
self.sound_event = 'first_message_received'
elif self.control_focused:
self.sound_event = 'next_message_received_focused'
else:
self.sound_event = 'next_message_received_unfocused'
if app.settings.get('notification_preview_message'):
self.popup_text = msg_obj.msgtxt
if self.popup_text and (self.popup_text.startswith('/me ') or \
self.popup_text.startswith('/me\n')):
self.popup_text = '* ' + nick + self.popup_text[3:]
else:
# We don't want message preview, do_preview = False
self.popup_text = ''
if msg_obj.properties.is_muc_pm:
self.popup_msg_type = 'pm'
self.popup_event_type = _('New Private Message')
else: # chat message
self.popup_msg_type = 'chat'
self.popup_event_type = _('New Message')
num_unread = len(app.events.get_events(self.conn.name, self.jid,
['printed_' + self.popup_msg_type, self.popup_msg_type]))
self.popup_title = i18n.ngettext(
'New message from %(nickname)s',
'%(n_msgs)i unread messages from %(nickname)s',
num_unread) % {'nickname': nick, 'n_msgs': num_unread}
if app.settings.get('show_notifications'):
if self.first_unread or not self.control_focused:
if app.settings.get('autopopupaway'):
# always show notification
self.do_popup = True
if app.connections[self.conn.name].status in ('online', 'chat'):
# we're online or chat
self.do_popup = True
if msg_obj.properties.attention and not app.settings.get(
'ignore_incoming_attention'):
self.popup_timeout = 0
self.do_popup = True
else:
self.popup_timeout = app.settings.get('notification_timeout')
sound = app.settings.get_soundevent_settings('attention_received')
if msg_obj.properties.attention and not app.settings.get(
'ignore_incoming_attention') and sound['enabled']:
self.sound_event = 'attention_received'
self.do_sound = True
elif self.first_unread and helpers.allow_sound_notification(
self.conn.name, 'first_message_received'):
self.do_sound = True
elif not self.first_unread and self.control_focused and \
helpers.allow_sound_notification(self.conn.name,
'next_message_received_focused'):
self.do_sound = True
elif not self.first_unread and not self.control_focused and \
helpers.allow_sound_notification(self.conn.name,
'next_message_received_unfocused'):
self.do_sound = True
def handle_incoming_gc_msg_event(self, msg_obj):
if not msg_obj.gc_control:
# we got a message from a room we're not in? ignore it
return
self.jid = msg_obj.jid
sound = msg_obj.gc_control.highlighting_for_message(
msg_obj.msgtxt, msg_obj.properties.timestamp)[1]
nick = msg_obj.properties.muc_nickname
if nick == msg_obj.gc_control.nick:
# A message from ourself
return
self.do_sound = True
if sound == 'received':
self.sound_event = 'muc_message_received'
elif sound == 'highlight':
self.sound_event = 'muc_message_highlight'
else:
self.do_sound = False
self.control = app.interface.msg_win_mgr.search_control(
msg_obj.jid, self.account)
if self.control is not None:
self.control_focused = self.control.has_focus()
if app.settings.get('show_notifications'):
contact = app.contacts.get_groupchat_contact(self.account,
self.jid)
notify_for_muc = sound == 'highlight' or contact.can_notify()
if not notify_for_muc:
self.do_popup = False
elif self.control_focused:
self.do_popup = False
elif app.settings.get('autopopupaway'):
# always show notification
self.do_popup = True
elif app.connections[self.conn.name].status in ('online', 'chat'):
# we're online or chat
self.do_popup = True
self.popup_msg_type = 'gc_msg'
self.popup_event_type = _('New Group Chat Message')
if app.settings.get('notification_preview_message'):
self.popup_text = msg_obj.msgtxt
if self.popup_text and (self.popup_text.startswith('/me ') or
self.popup_text.startswith('/me\n')):
self.popup_text = '* ' + nick + self.popup_text[3:]
type_events = ['printed_marked_gc_msg', 'printed_gc_msg']
count = len(app.events.get_events(self.account, self.jid, type_events))
contact = app.contacts.get_contact(self.account, self.jid)
self.popup_title = i18n.ngettext(
'New message from %(nickname)s',
'%(n_msgs)i unread messages in %(groupchat_name)s',
count) % {'nickname': nick,
'n_msgs': count,
'groupchat_name': contact.get_shown_name()}
def handle_incoming_pres_event(self, pres_obj):
if app.jid_is_transport(pres_obj.jid):
return True
account = pres_obj.conn.name
self.jid = pres_obj.jid
resource = pres_obj.resource or ''
# It isn't an agent
for c in pres_obj.contact_list:
if c.resource == resource:
# we look for other connected resources
continue
if c.show not in ('offline', 'error'):
return True
# no other resource is connected, let's look in metacontacts
family = app.contacts.get_metacontacts_family(account, self.jid)
for info in family:
acct_ = info['account']
jid_ = info['jid']
c_ = app.contacts.get_contact_with_highest_priority(acct_, jid_)
if not c_:
continue
if c_.jid == self.jid:
continue
if c_.show not in ('offline', 'error'):
return True
if pres_obj.old_show < 2 and pres_obj.new_show > 1:
event = 'contact_connected'
server = app.get_server_from_jid(self.jid)
account_server = account + '/' + server
block_transport = False
if account_server in app.block_signed_in_notifications and \
app.block_signed_in_notifications[account_server]:
block_transport = True
sound = app.settings.get_soundevent_settings('contact_connected')
if sound['enabled'] and not app.block_signed_in_notifications[account] and\
not block_transport and helpers.allow_sound_notification(account,
'contact_connected'):
self.sound_event = event
self.do_sound = True
elif pres_obj.old_show > 1 and pres_obj.new_show < 2:
event = 'contact_disconnected'
sound = app.settings.get_soundevent_settings('contact_disconnected')
if sound['enabled'] and helpers.allow_sound_notification(account, event):
self.sound_event = event
self.do_sound = True
# Status change (not connected/disconnected or error (<1))
elif pres_obj.new_show > 1:
event = 'status_change'
else:
return True
if app.jid_is_transport(self.jid):
self.transport_name = app.get_transport_name_from_jid(self.jid)
self.show = pres_obj.show
self.popup_timeout = app.settings.get('notification_timeout')
nick = i18n.direction_mark + app.get_name_from_jid(account, self.jid)
if event == 'status_change':
self.popup_title = _('%(nick)s Changed Status') % \
{'nick': nick}
self.popup_text = _('%(nick)s is now %(status)s') % \
{'nick': nick, 'status': helpers.get_uf_show(pres_obj.show)}
if pres_obj.status:
self.popup_text = self.popup_text + " : " + pres_obj.status
self.popup_event_type = _('Contact Changed Status')
class InformationEvent(nec.NetworkIncomingEvent):
name = 'information'
def init(self):
self.args = None
self.kwargs = {}
self.dialog_name = None
self.popup = True
def generate(self):
if self.args is None:
self.args = ()
else:
self.args = (self.args,)
return True

1115
gajim/common/const.py Normal file

File diff suppressed because it is too large Load Diff

991
gajim/common/contacts.py Normal file
View File

@ -0,0 +1,991 @@
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
# Tomasz Melcer <liori AT exroot.org>
# Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from functools import partial
try:
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.account import Account
from gajim import common
from gajim.common.const import Chatstate
except ImportError as e:
if __name__ != "__main__":
raise ImportError(str(e))
class ContactSettings:
def __init__(self, account, jid):
self.get = partial(app.settings.get_contact_setting, account, jid)
self.set = partial(app.settings.set_contact_setting, account, jid)
class GroupChatSettings:
def __init__(self, account, jid):
self.get = partial(app.settings.get_group_chat_setting, account, jid)
self.set = partial(app.settings.set_group_chat_setting, account, jid)
class XMPPEntity:
"""
Base representation of entities in XMPP
"""
def __init__(self, jid, account, resource):
self.jid = jid
self.resource = resource
self.account = account
class CommonContact(XMPPEntity):
def __init__(self, jid, account, resource, show, presence, status, name,
chatstate):
XMPPEntity.__init__(self, jid, account, resource)
self._show = show
self._presence = presence
self.status = status
self.name = name
# this is contact's chatstate
self._chatstate = chatstate
self._is_pm_contact = False
@property
def show(self):
return self._show
@show.setter
def show(self, value):
self._show = value
@property
def presence(self):
return self._presence
@presence.setter
def presence(self, value):
self._presence = value
@property
def is_available(self):
return self._presence.is_available
@property
def chatstate_enum(self):
return self._chatstate
@property
def chatstate(self):
if self._chatstate is None:
return
return str(self._chatstate)
@chatstate.setter
def chatstate(self, value):
if value is None:
self._chatstate = value
else:
self._chatstate = Chatstate[value.upper()]
@property
def is_gc_contact(self):
return isinstance(self, GC_Contact)
@property
def is_pm_contact(self):
return self._is_pm_contact
@property
def is_groupchat(self):
return False
def get_full_jid(self):
raise NotImplementedError
def get_shown_name(self):
raise NotImplementedError
def supports(self, requested_feature):
"""
Return True if the contact has advertised to support the feature
identified by the given namespace. False otherwise.
"""
if self.show == 'offline':
# Unfortunately, if all resources are offline, the contact
# includes the last resource that was online. Check for its
# show, so we can be sure it's existent. Otherwise, we still
# return caps for a contact that has no resources left.
return False
disco_info = app.storage.cache.get_last_disco_info(self.get_full_jid())
if disco_info is None:
return False
return disco_info.supports(requested_feature)
@property
def uses_phone(self):
disco_info = app.storage.cache.get_last_disco_info(self.get_full_jid())
if disco_info is None:
return False
return disco_info.has_category('phone')
class Contact(CommonContact):
"""
Information concerning a contact
"""
def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0,
chatstate=None, idle_time=None, avatar_sha=None, groupchat=False,
is_pm_contact=False):
if not isinstance(jid, str):
print('no str')
if groups is None:
groups = []
CommonContact.__init__(self, jid, account, resource, show,
None, status, name, chatstate)
self.contact_name = '' # nick chosen by contact
self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
self.avatar_sha = avatar_sha
self._is_groupchat = groupchat
self._is_pm_contact = is_pm_contact
if groupchat:
self.settings = GroupChatSettings(account.name, jid)
else:
self.settings = ContactSettings(account.name, jid)
self.sub = sub
self.ask = ask
self.priority = priority
self.idle_time = idle_time
self.pep = {}
def connect_signal(self, setting, func):
app.settings.connect_signal(
setting, func, self.account.name, self.jid)
def get_full_jid(self):
if self.resource:
return self.jid + '/' + self.resource
return self.jid
def get_shown_name(self):
if self._is_groupchat:
return self._get_groupchat_name()
if self.name:
return self.name
if self.contact_name:
return self.contact_name
return self.jid.split('@')[0]
def _get_groupchat_name(self):
from gajim.common.helpers import get_groupchat_name
con = app.connections[self.account.name]
return get_groupchat_name(con, self.jid)
def get_shown_groups(self):
if self.is_observer():
return [_('Observers')]
if self.is_groupchat:
return [_('Group chats')]
if self.is_transport():
return [_('Transports')]
if not self.groups:
return [_('General')]
return self.groups
def is_hidden_from_roster(self):
"""
If contact should not be visible in roster
"""
# XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
if self.is_transport():
return False
if self.sub in ('both', 'to'):
return False
if self.sub in ('none', 'from') and self.ask == 'subscribe':
return False
if self.sub in ('none', 'from') and (self.name or self.groups):
return False
if _('Not in contact list') in self.groups:
return False
return True
def is_observer(self):
# XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
is_observer = False
if self.sub == 'from' and not self.is_transport()\
and self.is_hidden_from_roster():
is_observer = True
return is_observer
@property
def is_groupchat(self):
return self._is_groupchat
@property
def is_connected(self):
try:
return app.gc_connected[self.account.name][self.jid]
except Exception:
return False
def is_transport(self):
# if not '@' or '@' starts the jid then contact is transport
return self.jid.find('@') <= 0
def can_notify(self):
if not self.is_groupchat:
raise ValueError
all_ = app.settings.get('notify_on_all_muc_messages')
room = self.settings.get('notify_on_all_messages')
return all_ or room
class GC_Contact(CommonContact):
"""
Information concerning each groupchat contact
"""
def __init__(self, room_jid, account, name='', show='', presence=None,
status='', role='', affiliation='', jid='', resource='',
chatstate=None, avatar_sha=None):
CommonContact.__init__(self, jid, account, resource, show,
presence, status, name, chatstate)
self.room_jid = room_jid
self.role = role
self.affiliation = affiliation
self.avatar_sha = avatar_sha
self.settings = ContactSettings(account.name, jid)
def get_full_jid(self):
return self.room_jid + '/' + self.name
def get_shown_name(self):
return self.name
def get_avatar(self, *args, **kwargs):
return common.app.interface.get_avatar(self, *args, **kwargs)
def as_contact(self):
"""
Create a Contact instance from this GC_Contact instance
"""
return Contact(jid=self.get_full_jid(), account=self.account,
name=self.name, groups=[], show=self.show, status=self.status,
sub='none', avatar_sha=self.avatar_sha,
is_pm_contact=True)
class LegacyContactsAPI:
"""
This is a GOD class for accessing contact and groupchat information.
The API has several flaws:
* it mixes concerns because it deals with contacts, groupchats,
groupchat contacts and metacontacts
* some methods like get_contact() may return None. This leads to
a lot of duplication all over Gajim because it is not sure
if we receive a proper contact or just None.
It is a long way to cleanup this API. Therefore just stick with it
and use it as before. We will try to figure out a migration path.
"""
def __init__(self):
self._metacontact_manager = MetacontactManager(self)
self._accounts = {}
def add_account(self, account_name):
self._accounts[account_name] = Account(account_name, Contacts(),
GC_Contacts())
self._metacontact_manager.add_account(account_name)
def get_accounts(self, zeroconf=True):
accounts = list(self._accounts.keys())
if not zeroconf:
if 'Local' in accounts:
accounts.remove('Local')
return accounts
def remove_account(self, account):
del self._accounts[account]
self._metacontact_manager.remove_account(account)
def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0,
chatstate=None, idle_time=None,
avatar_sha=None, groupchat=False):
if groups is None:
groups = []
# Use Account object if available
account = self._accounts.get(account, account)
return Contact(jid=jid, account=account, name=name, groups=groups,
show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority,
chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
groupchat=groupchat)
def create_self_contact(self, jid, account, resource, show, status, priority,
name=''):
conn = common.app.connections[account]
nick = name or common.app.nicks[account]
account = self._accounts.get(account, account) # Use Account object if available
self_contact = self.create_contact(jid=jid, account=account,
name=nick, groups=['self_contact'], show=show, status=status,
sub='both', ask='none', priority=priority,
resource=resource)
self_contact.pep = conn.pep
return self_contact
def create_not_in_roster_contact(self, jid, account, resource='', name='',
groupchat=False):
# Use Account object if available
account = self._accounts.get(account, account)
return self.create_contact(jid=jid, account=account, resource=resource,
name=name, groups=[_('Not in contact list')], show='not in roster',
status='', sub='none', groupchat=groupchat)
def copy_contact(self, contact):
return self.create_contact(contact.jid, contact.account,
name=contact.name, groups=contact.groups, show=contact.show,
status=contact.status, sub=contact.sub, ask=contact.ask,
resource=contact.resource, priority=contact.priority,
chatstate=contact.chatstate_enum,
idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
def add_contact(self, account, contact):
if account not in self._accounts:
self.add_account(account)
return self._accounts[account].contacts.add_contact(contact)
def remove_contact(self, account, contact):
if account not in self._accounts:
return
return self._accounts[account].contacts.remove_contact(contact)
def remove_jid(self, account, jid, remove_meta=True):
self._accounts[account].contacts.remove_jid(jid)
if remove_meta:
self._metacontact_manager.remove_metacontact(account, jid)
def get_groupchat_contact(self, account, jid):
return self._accounts[account].contacts.get_groupchat_contact(jid)
def get_contacts(self, account, jid):
return self._accounts[account].contacts.get_contacts(jid)
def get_contact(self, account, jid, resource=None):
return self._accounts[account].contacts.get_contact(jid, resource=resource)
def get_contact_strict(self, account, jid, resource):
return self._accounts[account].contacts.get_contact_strict(jid, resource)
def get_avatar(self, account, *args, **kwargs):
return self._accounts[account].contacts.get_avatar(*args, **kwargs)
def get_avatar_sha(self, account, jid):
return self._accounts[account].contacts.get_avatar_sha(jid)
def set_avatar(self, account, jid, sha):
self._accounts[account].contacts.set_avatar(jid, sha)
def iter_contacts(self, account):
for contact in self._accounts[account].contacts.iter_contacts():
yield contact
def get_contact_from_full_jid(self, account, fjid):
return self._accounts[account].contacts.get_contact_from_full_jid(fjid)
def get_first_contact_from_jid(self, account, jid):
return self._accounts[account].contacts.get_first_contact_from_jid(jid)
def get_contacts_from_group(self, account, group):
return self._accounts[account].contacts.get_contacts_from_group(group)
def get_contacts_jid_list(self, account):
return self._accounts[account].contacts.get_contacts_jid_list()
def get_jid_list(self, account):
return self._accounts[account].contacts.get_jid_list()
def change_contact_jid(self, old_jid, new_jid, account):
return self._accounts[account].change_contact_jid(old_jid, new_jid)
def get_highest_prio_contact_from_contacts(self, contacts):
if not contacts:
return None
prim_contact = contacts[0]
for contact in contacts[1:]:
if int(contact.priority) > int(prim_contact.priority):
prim_contact = contact
return prim_contact
def get_contact_with_highest_priority(self, account, jid):
contacts = self.get_contacts(account, jid)
if not contacts and '/' in jid:
# jid may be a fake jid, try it
room, nick = jid.split('/', 1)
contact = self.get_gc_contact(account, room, nick)
return contact
return self.get_highest_prio_contact_from_contacts(contacts)
def get_nb_online_total_contacts(self, accounts=None, groups=None):
"""
Return the number of online contacts and the total number of contacts
"""
if not accounts:
accounts = self.get_accounts()
if groups is None:
groups = []
nbr_online = 0
nbr_total = 0
for account in accounts:
our_jid = common.app.get_jid_from_account(account)
for jid in self.get_jid_list(account):
if jid == our_jid:
continue
if (common.app.jid_is_transport(jid) and
_('Transports') not in groups):
# do not count transports
continue
if self.has_brother(account, jid, accounts) and not \
self.is_big_brother(account, jid, accounts):
# count metacontacts only once
continue
contact = self._accounts[account].contacts._contacts[jid][0]
if _('Not in contact list') in contact.groups:
continue
in_groups = False
if groups == []:
in_groups = True
else:
for group in groups:
if group in contact.get_shown_groups():
in_groups = True
break
if in_groups:
if contact.show not in ('offline', 'error'):
nbr_online += 1
nbr_total += 1
return nbr_online, nbr_total
def __getattr__(self, attr_name):
# Only called if self has no attr_name
if hasattr(self._metacontact_manager, attr_name):
return getattr(self._metacontact_manager, attr_name)
raise AttributeError(attr_name)
def create_gc_contact(self, room_jid, account, name='', show='',
presence=None, status='', role='',
affiliation='', jid='', resource='', avatar_sha=None):
account = self._accounts.get(account, account) # Use Account object if available
return GC_Contact(room_jid, account, name, show, presence, status,
role, affiliation, jid, resource, avatar_sha=avatar_sha)
def add_gc_contact(self, account, gc_contact):
return self._accounts[account].gc_contacts.add_gc_contact(gc_contact)
def remove_gc_contact(self, account, gc_contact):
return self._accounts[account].gc_contacts.remove_gc_contact(gc_contact)
def remove_room(self, account, room_jid):
return self._accounts[account].gc_contacts.remove_room(room_jid)
def get_gc_list(self, account):
return self._accounts[account].gc_contacts.get_gc_list()
def get_nick_list(self, account, room_jid):
return self._accounts[account].gc_contacts.get_nick_list(room_jid)
def get_gc_contact_list(self, account, room_jid):
return self._accounts[account].gc_contacts.get_gc_contact_list(room_jid)
def get_gc_contact(self, account, room_jid, nick):
return self._accounts[account].gc_contacts.get_gc_contact(room_jid, nick)
def is_gc_contact(self, account, jid):
return self._accounts[account].gc_contacts.is_gc_contact(jid)
def get_nb_role_total_gc_contacts(self, account, room_jid, role):
return self._accounts[account].gc_contacts.get_nb_role_total_gc_contacts(room_jid, role)
def set_gc_avatar(self, account, room_jid, nick, sha):
contact = self.get_gc_contact(account, room_jid, nick)
if contact is None:
return
contact.avatar_sha = sha
def get_combined_chatstate(self, account, jid):
return self._accounts[account].contacts.get_combined_chatstate(jid)
class Contacts():
"""
This is a breakout of the contact related behavior of the old
Contacts class (which is not called LegacyContactsAPI)
"""
def __init__(self):
# list of contacts {jid1: [C1, C2]}, } one Contact per resource
self._contacts = {}
def add_contact(self, contact):
if contact.jid not in self._contacts or contact.is_groupchat:
self._contacts[contact.jid] = [contact]
return
contacts = self._contacts[contact.jid]
# We had only one that was offline, remove it
if len(contacts) == 1 and contacts[0].show == 'offline':
# Do not use self.remove_contact: it deletes
# self._contacts[account][contact.jid]
contacts.remove(contacts[0])
# If same JID with same resource already exists, use the new one
for c in contacts:
if c.resource == contact.resource:
self.remove_contact(c)
break
contacts.append(contact)
def remove_contact(self, contact):
if contact.jid not in self._contacts:
return
if contact in self._contacts[contact.jid]:
self._contacts[contact.jid].remove(contact)
if not self._contacts[contact.jid]:
del self._contacts[contact.jid]
def remove_jid(self, jid):
"""
Remove all contacts for a given jid
"""
if jid in self._contacts:
del self._contacts[jid]
def get_contacts(self, jid):
"""
Return the list of contact instances for this jid
"""
return list(self._contacts.get(jid, []))
def get_contact(self, jid, resource=None):
### WARNING ###
# This function returns a *RANDOM* resource if resource = None!
# Do *NOT* use if you need to get the contact to which you
# send a message for example, as a bare JID in XMPP means
# highest available resource, which this function ignores!
"""
Return the contact instance for the given resource if it's given else the
first contact is no resource is given or None if there is not
"""
if jid in self._contacts:
if not resource:
return self._contacts[jid][0]
for c in self._contacts[jid]:
if c.resource == resource:
return c
return self._contacts[jid][0]
def get_contact_strict(self, jid, resource):
"""
Return the contact instance for the given resource or None
"""
if jid in self._contacts:
for c in self._contacts[jid]:
if c.resource == resource:
return c
def get_groupchat_contact(self, jid):
if jid in self._contacts:
contacts = self._contacts[jid]
if contacts[0].is_groupchat:
return contacts[0]
def get_avatar(self, jid, size, scale, show=None):
if jid not in self._contacts:
return None
for resource in self._contacts[jid]:
avatar = common.app.interface.get_avatar(
resource, size, scale, show)
if avatar is None:
self.set_avatar(jid, None)
return avatar
def get_avatar_sha(self, jid):
if jid not in self._contacts:
return None
for resource in self._contacts[jid]:
if resource.avatar_sha is not None:
return resource.avatar_sha
return None
def set_avatar(self, jid, sha):
if jid not in self._contacts:
return
for resource in self._contacts[jid]:
resource.avatar_sha = sha
def iter_contacts(self):
for jid in list(self._contacts.keys()):
for contact in self._contacts[jid][:]:
yield contact
def get_jid_list(self):
return list(self._contacts.keys())
def get_contacts_jid_list(self):
return [jid for jid, contact in self._contacts.items() if not
contact[0].is_groupchat]
def get_contact_from_full_jid(self, fjid):
"""
Get Contact object for specific resource of given jid
"""
barejid, resource = common.app.get_room_and_nick_from_fjid(fjid)
return self.get_contact_strict(barejid, resource)
def get_first_contact_from_jid(self, jid):
if jid in self._contacts:
return self._contacts[jid][0]
def get_contacts_from_group(self, group):
"""
Return all contacts in the given group
"""
group_contacts = []
for jid in self._contacts:
contacts = self.get_contacts(jid)
if group in contacts[0].groups:
group_contacts += contacts
return group_contacts
def change_contact_jid(self, old_jid, new_jid):
if old_jid not in self._contacts:
return
self._contacts[new_jid] = []
for _contact in self._contacts[old_jid]:
_contact.jid = new_jid
self._contacts[new_jid].append(_contact)
del self._contacts[old_jid]
def get_combined_chatstate(self, jid):
if jid not in self._contacts:
return
contacts = self._contacts[jid]
states = []
for contact in contacts:
if contact.chatstate_enum is None:
continue
states.append(contact.chatstate_enum)
return str(min(states)) if states else None
class GC_Contacts():
def __init__(self):
# list of contacts that are in gc {room_jid: {nick: C}}}
self._rooms = {}
def add_gc_contact(self, gc_contact):
if gc_contact.room_jid not in self._rooms:
self._rooms[gc_contact.room_jid] = {gc_contact.name: gc_contact}
else:
self._rooms[gc_contact.room_jid][gc_contact.name] = gc_contact
def remove_gc_contact(self, gc_contact):
if gc_contact.room_jid not in self._rooms:
return
if gc_contact.name not in self._rooms[gc_contact.room_jid]:
return
del self._rooms[gc_contact.room_jid][gc_contact.name]
# It was the last nick in room ?
if not self._rooms[gc_contact.room_jid]:
del self._rooms[gc_contact.room_jid]
def remove_room(self, room_jid):
if room_jid in self._rooms:
del self._rooms[room_jid]
def get_gc_list(self):
return self._rooms.keys()
def get_nick_list(self, room_jid):
gc_list = self.get_gc_list()
if not room_jid in gc_list:
return []
return list(self._rooms[room_jid].keys())
def get_gc_contact_list(self, room_jid):
try:
return list(self._rooms[room_jid].values())
except Exception:
return []
def get_gc_contact(self, room_jid, nick):
try:
return self._rooms[room_jid][nick]
except KeyError:
return None
def is_gc_contact(self, jid):
"""
>>> gc = GC_Contacts()
>>> gc._rooms = {'gajim@conference.gajim.org' : {'test' : True}}
>>> gc.is_gc_contact('gajim@conference.gajim.org/test')
True
>>> gc.is_gc_contact('test@jabbim.com')
False
"""
jid = jid.split('/')
if len(jid) != 2:
return False
gcc = self.get_gc_contact(jid[0], jid[1])
return gcc is not None
def get_nb_role_total_gc_contacts(self, room_jid, role):
"""
Return the number of group chat contacts for the given role and the total
number of group chat contacts
"""
if room_jid not in self._rooms:
return 0, 0
nb_role = nb_total = 0
for nick in self._rooms[room_jid]:
if self._rooms[room_jid][nick].role == role:
nb_role += 1
nb_total += 1
return nb_role, nb_total
class MetacontactManager():
def __init__(self, contacts):
self._metacontacts_tags = {}
self._contacts = contacts
def add_account(self, account):
if account not in self._metacontacts_tags:
self._metacontacts_tags[account] = {}
def remove_account(self, account):
del self._metacontacts_tags[account]
def define_metacontacts(self, account, tags_list):
self._metacontacts_tags[account] = tags_list
def _get_new_metacontacts_tag(self, jid):
if not jid in self._metacontacts_tags:
return jid
#FIXME: can this append ?
assert False
def iter_metacontacts_families(self, account):
for tag in self._metacontacts_tags[account]:
family = self._get_metacontacts_family_from_tag(account, tag)
yield family
def _get_metacontacts_tag(self, account, jid):
"""
Return the tag of a jid
"""
if not account in self._metacontacts_tags:
return None
for tag in self._metacontacts_tags[account]:
for data in self._metacontacts_tags[account][tag]:
if data['jid'] == jid:
return tag
return None
def add_metacontact(self, brother_account, brother_jid, account, jid, order=None):
tag = self._get_metacontacts_tag(brother_account, brother_jid)
if not tag:
tag = self._get_new_metacontacts_tag(brother_jid)
self._metacontacts_tags[brother_account][tag] = [{'jid': brother_jid,
'tag': tag}]
if brother_account != account:
con = common.app.connections[brother_account]
con.get_module('MetaContacts').store_metacontacts(
self._metacontacts_tags[brother_account])
# be sure jid has no other tag
old_tag = self._get_metacontacts_tag(account, jid)
while old_tag:
self.remove_metacontact(account, jid)
old_tag = self._get_metacontacts_tag(account, jid)
if tag not in self._metacontacts_tags[account]:
self._metacontacts_tags[account][tag] = [{'jid': jid, 'tag': tag}]
else:
if order:
self._metacontacts_tags[account][tag].append({'jid': jid,
'tag': tag, 'order': order})
else:
self._metacontacts_tags[account][tag].append({'jid': jid,
'tag': tag})
con = common.app.connections[account]
con.get_module('MetaContacts').store_metacontacts(
self._metacontacts_tags[account])
def remove_metacontact(self, account, jid):
if account not in self._metacontacts_tags:
return
found = None
for tag in self._metacontacts_tags[account]:
for data in self._metacontacts_tags[account][tag]:
if data['jid'] == jid:
found = data
break
if found:
self._metacontacts_tags[account][tag].remove(found)
con = common.app.connections[account]
con.get_module('MetaContacts').store_metacontacts(
self._metacontacts_tags[account])
break
def has_brother(self, account, jid, accounts):
tag = self._get_metacontacts_tag(account, jid)
if not tag:
return False
meta_jids = self._get_metacontacts_jids(tag, accounts)
return len(meta_jids) > 1 or len(meta_jids[account]) > 1
def is_big_brother(self, account, jid, accounts):
family = self.get_metacontacts_family(account, jid)
if family:
nearby_family = [data for data in family
if account in accounts]
bb_data = self._get_metacontacts_big_brother(nearby_family)
if bb_data['jid'] == jid and bb_data['account'] == account:
return True
return False
def _get_metacontacts_jids(self, tag, accounts):
"""
Return all jid for the given tag in the form {acct: [jid1, jid2],.}
"""
answers = {}
for account in self._metacontacts_tags:
if tag in self._metacontacts_tags[account]:
if account not in accounts:
continue
answers[account] = []
for data in self._metacontacts_tags[account][tag]:
answers[account].append(data['jid'])
return answers
def get_metacontacts_family(self, account, jid):
"""
Return the family of the given jid, including jid in the form:
[{'account': acct, 'jid': jid, 'order': order}, ] 'order' is optional
"""
tag = self._get_metacontacts_tag(account, jid)
return self._get_metacontacts_family_from_tag(account, tag)
def _get_metacontacts_family_from_tag(self, account, tag):
if not tag:
return []
answers = []
if tag in self._metacontacts_tags[account]:
for data in self._metacontacts_tags[account][tag]:
data['account'] = account
answers.append(data)
return answers
def _metacontact_key(self, data):
"""
Data is {'jid': jid, 'account': account, 'order': order} order is
optional
"""
show_list = ['not in roster', 'error', 'offline', 'dnd',
'xa', 'away', 'chat', 'online', 'requested', 'message']
jid = data['jid']
account = data['account']
# contact can be null when a jid listed in the metacontact data
# is not in our roster
contact = self._contacts.get_contact_with_highest_priority(
account, jid)
show = show_list.index(contact.show) if contact else 0
priority = contact.priority if contact else 0
has_order = 'order' in data
order = data.get('order', 0)
transport = common.app.get_transport_name_from_jid(jid)
server = common.app.get_server_from_jid(jid)
myserver = app.settings.get_account_setting(account, 'hostname')
return (bool(contact), show > 2, has_order, order, bool(transport),
show, priority, server == myserver, jid, account)
def get_nearby_family_and_big_brother(self, family, account):
"""
Return the nearby family and its Big Brother
Nearby family is the part of the family that is grouped with the
metacontact. A metacontact may be over different accounts. If accounts
are not merged then the given family is split account wise.
(nearby_family, big_brother_jid, big_brother_account)
"""
if app.settings.get('mergeaccounts'):
# group all together
nearby_family = family
else:
# we want one nearby_family per account
nearby_family = [data for data in family if account == data['account']]
if not nearby_family:
return (None, None, None)
big_brother_data = self._get_metacontacts_big_brother(nearby_family)
big_brother_jid = big_brother_data['jid']
big_brother_account = big_brother_data['account']
return (nearby_family, big_brother_jid, big_brother_account)
def _get_metacontacts_big_brother(self, family):
"""
Which of the family will be the big brother under which all others will be
?
"""
return max(family, key=self._metacontact_key)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

View File

@ -0,0 +1,112 @@
# Copyright (C) 2009-2014 Yann Leboulanger <asterix AT lagaule.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
from datetime import datetime
from gi.repository import GLib
from nbxmpp.structs import LocationData
from gajim.common import app
if app.is_installed('GEOCLUE'):
from gi.repository import Geoclue # pylint: disable=ungrouped-imports,no-name-in-module
log = logging.getLogger('gajim.c.dbus.location')
class LocationListener:
_instance = None
@classmethod
def get(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
self._data = {}
self.location_info = {}
self.simple = None
# Note: do not remove third parameter `param`
# because notify signal expects three parameters
def _on_location_update(self, simple, _param=None):
location = simple.get_location()
timestamp = location.get_property("timestamp")[0]
lat = location.get_property("latitude")
lon = location.get_property("longitude")
alt = location.get_property("altitude")
# in XEP-0080 it's horizontal accuracy
acc = location.get_property("accuracy")
# update data with info we just received
self._data = {'lat': lat, 'lon': lon, 'alt': alt, 'accuracy': acc}
self._data['timestamp'] = self._timestamp_to_utc(timestamp)
self._send_location()
def _on_simple_ready(self, _obj, result):
try:
self.simple = Geoclue.Simple.new_finish(result)
except GLib.Error as error:
log.warning("Could not enable geolocation: %s", error.message)
else:
self.simple.connect('notify::location', self._on_location_update)
self._on_location_update(self.simple)
def get_data(self):
Geoclue.Simple.new("org.gajim.Gajim",
Geoclue.AccuracyLevel.EXACT,
None,
self._on_simple_ready)
def start(self):
self.location_info = {}
self.get_data()
def _send_location(self):
accounts = app.connections.keys()
for acct in accounts:
if not app.account_is_available(acct):
continue
if not app.settings.get_account_setting(acct, 'publish_location'):
continue
if self.location_info == self._data:
continue
if 'timestamp' in self.location_info and 'timestamp' in self._data:
last_data = self.location_info.copy()
del last_data['timestamp']
new_data = self._data.copy()
del new_data['timestamp']
if last_data == new_data:
continue
app.connections[acct].get_module('UserLocation').set_location(
LocationData(**self._data))
self.location_info = self._data.copy()
@staticmethod
def _timestamp_to_utc(timestamp):
time = datetime.utcfromtimestamp(timestamp)
return time.strftime('%Y-%m-%dT%H:%MZ')
def enable():
if not app.is_installed('GEOCLUE'):
log.warning('GeoClue not installed')
return
listener = LocationListener.get()
listener.start()

142
gajim/common/dbus/logind.py Normal file
View File

@ -0,0 +1,142 @@
# Copyright (C) 2014 Kamil Paral <kamil.paral AT gmail.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
'''
Watch for system sleep using systemd-logind.
Documentation: http://www.freedesktop.org/wiki/Software/systemd/inhibit
'''
import os
import logging
from gi.repository import Gio
from gi.repository import GLib
from gajim.common import app
from gajim.common.i18n import _
log = logging.getLogger('gajim.c.dbus.logind')
class LogindListener:
_instance = None
@classmethod
def get(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
# file descriptor object of the inhibitor
self._inhibit_fd = None
Gio.bus_watch_name(
Gio.BusType.SYSTEM,
'org.freedesktop.login1',
Gio.BusNameWatcherFlags.NONE,
self._on_appear_logind,
self._on_vanish_logind)
def _on_prepare_for_sleep(self, connection, _sender_name, _object_path,
interface_name, signal_name, parameters,
*_user_data):
'''Signal handler for PrepareForSleep event'''
log.debug('Received signal %s.%s%s',
interface_name, signal_name, parameters)
before = parameters[0] # Signal is either before or after sleep occurs
if before:
warn = self._inhibit_fd is None
log.log(
logging.WARNING if warn else logging.INFO,
'Preparing for sleep by disconnecting from network%s',
', without holding a sleep inhibitor' if warn else '')
for name, conn in app.connections.items():
if app.account_is_connected(name):
st = conn.status_message
conn.change_status('offline',
_('Machine is going to sleep'))
# TODO: Make this nicer
conn._status_message = st # pylint: disable=protected-access
conn.time_to_reconnect = 5
self._disinhibit_sleep()
else:
try:
self._inhibit_sleep(connection)
except GLib.Error as error:
log.warning('Inhibit failed: %s', error)
for conn in app.connections.values():
if conn.state.is_disconnected and conn.time_to_reconnect:
conn.reconnect()
def _inhibit_sleep(self, connection):
'''Obtain a sleep delay inhibitor from logind'''
if self._inhibit_fd is not None:
# Something is wrong, we have an inhibitor fd, and we are asking for
# yet another one.
log.warning('Trying to obtain a sleep inhibitor '
'while already holding one.')
ret, ret_fdlist = connection.call_with_unix_fd_list_sync(
'org.freedesktop.login1',
'/org/freedesktop/login1',
'org.freedesktop.login1.Manager',
'Inhibit',
GLib.Variant('(ssss)', (
'sleep', 'org.gajim.Gajim', _('Disconnect from the network'),
'delay' # Inhibitor will delay but not block sleep
)),
GLib.VariantType.new('(h)'),
Gio.DBusCallFlags.NONE, -1, None, None)
log.info('Inhibit sleep')
self._inhibit_fd = ret_fdlist.get(ret.unpack()[0])
def _disinhibit_sleep(self):
'''Relinquish our sleep delay inhibitor'''
if self._inhibit_fd is not None:
os.close(self._inhibit_fd)
self._inhibit_fd = None
log.info('Disinhibit sleep')
def _on_appear_logind(self, connection, name, name_owner, *_user_data):
'''Use signal and locks provided by org.freedesktop.login1'''
log.info('Name %s appeared, owned by %s', name, name_owner)
connection.signal_subscribe(
'org.freedesktop.login1',
'org.freedesktop.login1.Manager',
'PrepareForSleep',
'/org/freedesktop/login1',
None,
Gio.DBusSignalFlags.NONE,
self._on_prepare_for_sleep,
None)
self._inhibit_sleep(connection)
def _on_vanish_logind(self, _connection, name, *_user_data):
'''Release remaining resources related to org.freedesktop.login1'''
log.info('Name %s vanished', name)
self._disinhibit_sleep()
def enable():
return
# LogindListener.get()

View File

@ -0,0 +1,208 @@
# Copyright (C) 2006 Gustavo Carneiro <gjcarneiro AT gmail.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2008 Jean-Marie Traissard <jim AT lapin.org>
# Jonathan Schleifer <js-gajim AT webkeks.org>
# Stephan Erb <steve-e AT h3c.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
from gi.repository import Gio
from gi.repository import GLib
from nbxmpp.structs import TuneData
from gajim.common import app
from gajim.common.nec import NetworkEvent
log = logging.getLogger('gajim.c.dbus.music_track')
MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.'
class MusicTrackListener:
_instance = None
@classmethod
def get(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
self.players = {}
self.connection = None
self._current_tune = None
def _emit(self, info):
self._current_tune = info
app.nec.push_incoming_event(
NetworkEvent('music-track-changed', info=info))
@property
def current_tune(self):
return self._current_tune
def start(self):
proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus',
None)
self.connection = proxy.get_connection()
self.connection.signal_subscribe(
'org.freedesktop.DBus',
'org.freedesktop.DBus',
'NameOwnerChanged',
'/org/freedesktop/DBus',
None,
Gio.DBusSignalFlags.NONE,
self._signal_name_owner_changed)
try:
result = proxy.call_sync(
'ListNames',
None,
Gio.DBusCallFlags.NONE,
-1,
None)
except GLib.Error as error:
log.debug("Could not list names: %s", error.message)
return
for name in result[0]:
if name.startswith(MPRIS_PLAYER_PREFIX):
self._add_player(name)
for name in list(self.players):
self._get_playing_track(name)
def stop(self):
for name in list(self.players):
if name.startswith(MPRIS_PLAYER_PREFIX):
self._remove_player(name)
def _signal_name_owner_changed(self,
_connection,
_sender_name,
_object_path,
_interface_name,
_signal_name,
parameters,
*_user_data):
name, old_owner, new_owner = parameters
if name.startswith(MPRIS_PLAYER_PREFIX):
if new_owner and not old_owner:
self._add_player(name)
else:
self._remove_player(name)
def _add_player(self, name):
'''Set up a listener for music player signals'''
log.info('%s appeared', name)
if name in self.players:
return
self.players[name] = self.connection.signal_subscribe(
name,
'org.freedesktop.DBus.Properties',
'PropertiesChanged',
'/org/mpris/MediaPlayer2',
None,
Gio.DBusSignalFlags.NONE,
self._signal_received,
name)
def _remove_player(self, name):
log.info('%s vanished', name)
if name in self.players:
self.connection.signal_unsubscribe(
self.players[name])
self.players.pop(name)
self._emit(None)
def _signal_received(self,
_connection,
_sender_name,
_object_path,
interface_name,
_signal_name,
parameters,
*user_data):
'''Signal handler for PropertiesChanged event'''
log.info('Signal received: %s - %s', interface_name, parameters)
self._get_playing_track(user_data[0])
@staticmethod
def _get_music_info(properties):
meta = properties.get('Metadata')
if meta is None or not meta:
return None
status = properties.get('PlaybackStatus')
if status is None or status == 'Paused':
return None
title = meta.get('xesam:title')
album = meta.get('xesam:album')
# xesam:artist is always a list of strings if not None
artist = meta.get('xesam:artist')
if artist is not None:
artist = ', '.join(artist)
return TuneData(artist=artist, title=title, source=album)
def _get_playing_track(self, name):
'''Return a TuneData for the currently playing
song, or None if no song is playing'''
proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
name,
'/org/mpris/MediaPlayer2',
'org.freedesktop.DBus.Properties',
None)
def proxy_call_finished(proxy, res):
try:
result = proxy.call_finish(res)
except GLib.Error as error:
log.debug("Could not enable music listener: %s", error.message)
return
info = self._get_music_info(result[0])
if info is not None:
self._emit(info)
proxy.call("GetAll",
GLib.Variant('(s)', ('org.mpris.MediaPlayer2.Player',)),
Gio.DBusCallFlags.NONE,
-1,
None,
proxy_call_finished)
def enable():
listener = MusicTrackListener.get()
listener.start()

229
gajim/common/dh.py Normal file
View File

@ -0,0 +1,229 @@
# Copyright (C) 2007-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
"""
This module defines a number of constants; specifically, large primes suitable
for use with the Diffie-Hellman key exchange
These constants have been obtained from RFC2409 and RFC3526.
"""
import string
generators = [
None, # one to get the right offset
2,
2,
None,
None,
2,
None,
None,
None,
None,
None,
None,
None,
None,
2, # group 14
2,
2,
2,
2
]
_HEX_PRIMES = [
None,
# group 1
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A63A3620 FFFFFFFF FFFFFFFF''',
# group 2
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE65381
FFFFFFFF FFFFFFFF''',
# XXX how do I obtain these?
None,
None,
# group 5
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF''',
None,
None,
None,
None,
None,
None,
None,
None,
# group 14
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AACAA68 FFFFFFFF FFFFFFFF''',
# group 15
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF''',
# group 16
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199
FFFFFFFF FFFFFFFF''',
# group 17
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08
8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B
302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9
A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6
49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8
FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C
180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718
3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D
04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D
B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226
1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC
E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26
99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB
04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2
233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127
D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406
AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918
DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151
2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03
F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F
BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B
B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632
387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E
6DCC4024 FFFFFFFF FFFFFFFF''',
# group 18
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD
F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831
179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B
DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF
5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6
D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3
23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328
06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C
DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE
12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4
38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300
741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568
3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9
22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B
4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A
062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36
4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1
B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92
4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47
9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71
60C980DD 98EDD3DF FFFFFFFF FFFFFFFF'''
]
_ALL_ASCII = ''.join(map(chr, range(256)))
def hex_to_decimal(stripee):
if not stripee:
return None
return int(stripee.translate(_ALL_ASCII).translate(
str.maketrans("", "", string.whitespace)), 16)
primes = list(map(hex_to_decimal, _HEX_PRIMES))

464
gajim/common/events.py Normal file
View File

@ -0,0 +1,464 @@
# Copyright (C) 2006 Jean-Marie Traissard <jim AT lapin.org>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import time
from gajim.common import app
class Event:
"""
Information concerning each event
"""
def __init__(self, time_=None, show_in_roster=False, show_in_systray=True):
"""
type_ in chat, normal, file-request, file-error, file-completed,
file-request-error, file-send-error, file-stopped, gc_msg, pm,
printed_chat, printed_gc_msg, printed_marked_gc_msg, printed_pm,
gc-invitation, subscription_request, unsubscribedm jingle-incoming
parameters is (per type_):
chat, normal, pm: [message, subject, kind, time, encrypted, resource,
msg_log_id]
where kind in error, incoming
file-*: file_props
gc_msg: None
printed_chat: [message, subject, control, msg_log_id]
printed_*: None
messages that are already printed in chat, but not read
gc-invitation: [room_jid, reason, password, jid_from]
subscription_request: [text, nick]
unsubscribed: contact
jingle-incoming: (fulljid, sessionid, content_types)
"""
if time_:
self.time_ = time_
else:
self.time_ = time.time()
self.show_in_roster = show_in_roster
self.show_in_systray = show_in_systray
# Set when adding the event
self.jid = None
self.account = None
class ChatEvent(Event):
type_ = 'chat'
def __init__(self, message, subject, kind, time_, resource,
msg_log_id, correct_id=None, message_id=None, session=None,
displaymarking=None, sent_forwarded=False, show_in_roster=False,
show_in_systray=True, additional_data=None):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.message = message
self.subject = subject
self.kind = kind
self.time = time_
self.resource = resource
self.msg_log_id = msg_log_id
self.message_id = message_id
self.correct_id = correct_id
self.session = session
self.displaymarking = displaymarking
self.sent_forwarded = sent_forwarded
if additional_data is None:
from gajim.common.helpers import AdditionalDataDict
additional_data = AdditionalDataDict()
self.additional_data = additional_data
class PmEvent(ChatEvent):
type_ = 'pm'
class PrintedChatEvent(Event):
type_ = 'printed_chat'
def __init__(self, message, subject, control, msg_log_id, time_=None,
message_id=None, stanza_id=None, show_in_roster=False,
show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.message = message
self.subject = subject
self.control = control
self.msg_log_id = msg_log_id
self.message_id = message_id
self.stanza_id = stanza_id
class PrintedGcMsgEvent(PrintedChatEvent):
type_ = 'printed_gc_msg'
class PrintedMarkedGcMsgEvent(PrintedChatEvent):
type_ = 'printed_marked_gc_msg'
class PrintedPmEvent(PrintedChatEvent):
type_ = 'printed_pm'
class SubscriptionRequestEvent(Event):
type_ = 'subscription_request'
def __init__(self, text, nick, time_=None, show_in_roster=False,
show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.text = text
self.nick = nick
class UnsubscribedEvent(Event):
type_ = 'unsubscribed'
def __init__(self, contact, time_=None, show_in_roster=False,
show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.contact = contact
class GcInvitationtEvent(Event):
type_ = 'gc-invitation'
def __init__(self, event):
Event.__init__(self, None, show_in_roster=False, show_in_systray=True)
for key, value in vars(event).items():
setattr(self, key, value)
def get_inviter_name(self):
if self.from_.bare_match(self.muc):
return self.from_.resource
contact = app.contacts.get_first_contact_from_jid(
self.account, self.from_.bare)
if contact is None:
return str(self.from_)
return contact.get_shown_name()
class FileRequestEvent(Event):
type_ = 'file-request'
def __init__(self, file_props, time_=None, show_in_roster=False, show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.file_props = file_props
class FileSendErrorEvent(FileRequestEvent):
type_ = 'file-send-error'
class FileErrorEvent(FileRequestEvent):
type_ = 'file-error'
class FileRequestErrorEvent(FileRequestEvent):
type_ = 'file-request-error'
class FileCompletedEvent(FileRequestEvent):
type_ = 'file-completed'
class FileStoppedEvent(FileRequestEvent):
type_ = 'file-stopped'
class FileHashErrorEvent(FileRequestEvent):
type_ = 'file-hash-error'
class JingleIncomingEvent(Event):
type_ = 'jingle-incoming'
def __init__(self, peerjid, sid, content_types, time_=None, show_in_roster=False, show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.peerjid = peerjid
self.sid = sid
self.content_types = content_types
class Events:
"""
Information concerning all events
"""
def __init__(self):
self._events = {} # list of events {acct: {jid1: [E1, E2]}, }
self._event_added_listeners = []
self._event_removed_listeners = []
def event_added_subscribe(self, listener):
"""
Add a listener when an event is added to the queue
"""
if not listener in self._event_added_listeners:
self._event_added_listeners.append(listener)
def event_added_unsubscribe(self, listener):
"""
Remove a listener when an event is added to the queue
"""
if listener in self._event_added_listeners:
self._event_added_listeners.remove(listener)
def event_removed_subscribe(self, listener):
"""
Add a listener when an event is removed from the queue
"""
if not listener in self._event_removed_listeners:
self._event_removed_listeners.append(listener)
def event_removed_unsubscribe(self, listener):
"""
Remove a listener when an event is removed from the queue
"""
if listener in self._event_removed_listeners:
self._event_removed_listeners.remove(listener)
def fire_event_added(self, event):
for listener in self._event_added_listeners:
listener(event)
def fire_event_removed(self, event_list):
for listener in self._event_removed_listeners:
listener(event_list)
def add_account(self, account):
self._events[account] = {}
def get_accounts(self):
return self._events.keys()
def remove_account(self, account):
del self._events[account]
def add_event(self, account, jid, event):
# No such account before ?
if account not in self._events:
self._events[account] = {jid: [event]}
# no such jid before ?
elif jid not in self._events[account]:
self._events[account][jid] = [event]
else:
self._events[account][jid].append(event)
event.jid = jid
event.account = account
self.fire_event_added(event)
def remove_events(self, account, jid, event=None, types=None):
"""
If event is not specified, remove all events from this jid, optionally
only from given type return True if no such event found
"""
if types is None:
types = []
if account not in self._events:
return True
if jid not in self._events[account]:
return True
if event: # remove only one event
if event in self._events[account][jid]:
if len(self._events[account][jid]) == 1:
del self._events[account][jid]
else:
self._events[account][jid].remove(event)
self.fire_event_removed([event])
return
return True
if types:
new_list = [] # list of events to keep
removed_list = [] # list of removed events
for ev in self._events[account][jid]:
if ev.type_ not in types:
new_list.append(ev)
else:
removed_list.append(ev)
if len(new_list) == len(self._events[account][jid]):
return True
if new_list:
self._events[account][jid] = new_list
else:
del self._events[account][jid]
self.fire_event_removed(removed_list)
return
# No event nor type given, remove them all
removed_list = self._events[account][jid]
del self._events[account][jid]
self.fire_event_removed(removed_list)
def change_jid(self, account, old_jid, new_jid):
if account not in self._events:
return
if old_jid not in self._events[account]:
return
if new_jid in self._events[account]:
self._events[account][new_jid] += self._events[account][old_jid]
else:
self._events[account][new_jid] = self._events[account][old_jid]
del self._events[account][old_jid]
def get_nb_events(self, types=None, account=None):
if types is None:
types = []
return self._get_nb_events(types=types, account=account)
def get_events(self, account, jid=None, types=None):
"""
Return all events from the given account of the form {jid1: [], jid2:
[]}. If jid is given, returns all events from the given jid in a list: []
optionally only from given type
"""
if types is None:
types = []
if account not in self._events:
return []
if not jid:
events_list = {} # list of events
for jid_ in self._events[account]:
events = []
for ev in self._events[account][jid_]:
if not types or ev.type_ in types:
events.append(ev)
if events:
events_list[jid_] = events
return events_list
if jid not in self._events[account]:
return []
events_list = [] # list of events
for ev in self._events[account][jid]:
if not types or ev.type_ in types:
events_list.append(ev)
return events_list
def get_all_events(self, types=None):
accounts = self._events.keys()
events = []
for account in accounts:
for jid in self._events[account]:
for event in self._events[account][jid]:
if types is None or event.type_ in types:
events.append(event)
return events
def get_first_event(self, account=None, jid=None, type_=None):
"""
Return the first event of type type_ if given
"""
if not account:
return self._get_first_event_with_attribute(self._events)
events_list = self.get_events(account, jid, type_)
# be sure it's bigger than latest event
first_event_time = time.time() + 1
first_event = None
for event in events_list:
if event.time_ < first_event_time:
first_event_time = event.time_
first_event = event
return first_event
def _get_nb_events(self, account=None, jid=None, attribute=None, types=None):
"""
Return the number of pending events
"""
if types is None:
types = []
nb = 0
if account:
accounts = [account]
else:
accounts = self._events.keys()
for acct in accounts:
if acct not in self._events:
continue
if jid:
jids = [jid]
else:
jids = self._events[acct].keys()
for j in jids:
if j not in self._events[acct]:
continue
for event in self._events[acct][j]:
if types and event.type_ not in types:
continue
if not attribute or \
attribute == 'systray' and event.show_in_systray or \
attribute == 'roster' and event.show_in_roster:
nb += 1
return nb
def _get_some_events(self, attribute):
"""
Attribute in systray, roster
"""
events = {}
for account in self._events:
events[account] = {}
for jid in self._events[account]:
events[account][jid] = []
for event in self._events[account][jid]:
if attribute == 'systray' and event.show_in_systray or \
attribute == 'roster' and event.show_in_roster:
events[account][jid].append(event)
if not events[account][jid]:
del events[account][jid]
if not events[account]:
del events[account]
return events
def _get_first_event_with_attribute(self, events):
"""
Get the first event
events is in the form {account1: {jid1: [ev1, ev2], },. }
"""
# be sure it's bigger than latest event
first_event_time = time.time() + 1
first_account = None
first_jid = None
first_event = None
for account in events:
for jid in events[account]:
for event in events[account][jid]:
if event.time_ < first_event_time:
first_event_time = event.time_
first_account = account
first_jid = jid
first_event = event
return first_account, first_jid, first_event
def get_nb_systray_events(self, types=None):
"""
Return the number of events displayed in roster
"""
if types is None:
types = []
return self._get_nb_events(attribute='systray', types=types)
def get_systray_events(self):
"""
Return all events that must be displayed in systray:
{account1: {jid1: [ev1, ev2], },. }
"""
return self._get_some_events('systray')
def get_first_systray_event(self):
events = self.get_systray_events()
return self._get_first_event_with_attribute(events)
def get_nb_roster_events(self, account=None, jid=None, types=None):
"""
Return the number of events displayed in roster
"""
if types is None:
types = []
return self._get_nb_events(attribute='roster', account=account,
jid=jid, types=types)
def get_roster_events(self):
"""
Return all events that must be displayed in roster:
{account1: {jid1: [ev1, ev2], },. }
"""
return self._get_some_events('roster')

157
gajim/common/exceptions.py Normal file
View File

@ -0,0 +1,157 @@
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2005-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2006 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Brendan Taylor <whateley AT gmail.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from gajim.common.i18n import _
class PysqliteOperationalError(Exception):
"""
Sqlite2 raised pysqlite2.dbapi2.OperationalError
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class DatabaseMalformed(Exception):
"""
The database can't be read
"""
def __init__(self, path=''):
Exception.__init__(self)
self.path = path
def __str__(self):
return _('The database file (%s) cannot be read. '
'Try to repair it (see '
'https://dev.gajim.org/gajim/gajim/wikis/help/DatabaseBackup)'
' or remove it (all history will be lost).') % self.path
class ServiceNotAvailable(Exception):
"""
This exception is raised when we cannot use Gajim remotely'
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('Service not available: Gajim is not running, or remote_control is False')
class DbusNotSupported(Exception):
"""
D-Bus is not installed or python bindings are missing
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('D-Bus is not present on this machine or python module is missing')
class SessionBusNotPresent(Exception):
"""
This exception indicates that there is no session daemon
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('Session bus is not available.\nTry reading %(url)s') % \
{'url': 'https://dev.gajim.org/gajim/gajim/wikis/help/GajimDBus'}
class SystemBusNotPresent(Exception):
"""
This exception indicates that there is no session daemon
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('System bus is not available.\nTry reading %(url)s') % \
{'url': 'https://dev.gajim.org/gajim/gajim/wikis/help/GajimDBus'}
class NegotiationError(Exception):
"""
A session negotiation failed
"""
class Cancelled(Exception):
"""
The user cancelled an operation
"""
class LatexError(Exception):
"""
LaTeX processing failed for some reason
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class GajimGeneralException(Exception):
"""
This exception is our general exception
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class PluginsystemError(Exception):
"""
Error in the pluginsystem
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class StanzaMalformed(Exception):
"""
Malfromed Stanza
"""
def __init__(self, message, stanza=''):
Exception.__init__(self, message, stanza)
self._msg = '{}\n{}'.format(message, stanza)
def __str__(self):
return self._msg
class SendMessageError(Exception):
pass
class FileError(Exception):
pass

173
gajim/common/file_props.py Normal file
View File

@ -0,0 +1,173 @@
"""
This module is in charge of taking care of all the information related to
individual files. Files are identified by the account name and its sid.
>>> print(FilesProp.getFileProp('jabberid', '10'))
None
>>> fp = FilesProp()
Traceback (most recent call last):
...
Exception: this class should not be instatiated
>>> print(FilesProp.getAllFileProp())
[]
>>> fp = FilesProp.getNewFileProp('jabberid', '10')
>>> fp2 = FilesProp.getFileProp('jabberid', '10')
>>> fp == fp2
True
"""
from typing import Any # pylint: disable=unused-import
from typing import ClassVar # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import Tuple # pylint: disable=unused-import
class FilesProp:
_files_props = {} # type: ClassVar[Dict[Tuple[str, str], Any]]
def __init__(self):
raise Exception('this class should not be instantiated')
@classmethod
def getNewFileProp(cls, account, sid):
fp = FileProp(account, sid)
cls.setFileProp(fp, account, sid)
return fp
@classmethod
def getFileProp(cls, account, sid):
if (account, sid) in cls._files_props.keys():
return cls._files_props[account, sid]
@classmethod
def getFilePropByAccount(cls, account):
# Returns a list of file_props in one account
file_props = []
for account_, sid in cls._files_props:
if account_ == account:
file_props.append(cls._files_props[account, sid])
return file_props
@classmethod
def getFilePropByType(cls, type_, sid):
# This method should be deleted. Getting fileprop by type and sid is not
# unique enough. More than one fileprop might have the same type and sid
files_prop = cls.getAllFileProp()
for fp in files_prop:
if fp.type_ == type_ and fp.sid == sid:
return fp
@classmethod
def getFilePropBySid(cls, sid):
# This method should be deleted. It is kept to make things compatible
# This method should be replaced and instead get the file_props by
# account and sid
files_prop = cls.getAllFileProp()
for fp in files_prop:
if fp.sid == sid:
return fp
@classmethod
def getFilePropByTransportSid(cls, account, sid):
files_prop = cls.getAllFileProp()
for fp in files_prop:
if fp.account == account and fp.transport_sid == sid:
return fp
@classmethod
def getAllFileProp(cls):
return list(cls._files_props.values())
@classmethod
def setFileProp(cls, fp, account, sid):
cls._files_props[account, sid] = fp
@classmethod
def deleteFileProp(cls, file_prop):
files_props = cls._files_props
a = s = None
for key in files_props:
account, sid = key
fp = files_props[account, sid]
if fp is file_prop:
a = account
s = sid
if a is not None and s is not None:
del files_props[a, s]
class FileProp:
def __init__(self, account, sid):
# Do not instantiate this class directly. Call FilesProp.getNeFileProp
# instead
self.streamhosts = []
self.transfered_size = []
self.started = False
self.completed = False
self.paused = False
self.stalled = False
self.connected = False
self.stopped = False
self.is_a_proxy = False
self.proxyhost = None
self.proxy_sender = None
self.proxy_receiver = None
self.streamhost_used = None
# method callback called in case of transfer failure
self.failure_cb = None
# method callback called when disconnecting
self.disconnect_cb = None
self.continue_cb = None
self.sha_str = None
# transfer type: 's' for sending and 'r' for receiving
self.type_ = None
self.error = None
# Elapsed time of the file transfer
self.elapsed_time = 0
self.last_time = None
self.received_len = None
# full file path
self.file_name = None
self.name = None
self.date = None
self.desc = None
self.offset = None
self.sender = None
self.receiver = None
self.tt_account = None
self.size = None
self._sid = sid
self.transport_sid = None
self.account = account
self.mime_type = None
self.algo = None
self.direction = None
self.syn_id = None
self.seq = None
self.hash_ = None
self.fd = None
self.startexmpp = None
# Type of the session, if it is 'jingle' or 'si'
self.session_type = None
self.request_id = None
self.proxyhosts = None
self.dstaddr = None
def getsid(self):
# Getter of the property sid
return self._sid
def setsid(self, value):
# The sid value will change
# we need to change the in _files_props key as well
del FilesProp._files_props[self.account, self._sid]
self._sid = value
FilesProp._files_props[self.account, self._sid] = self
sid = property(getsid, setsid)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,110 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Dict
from gajim.common.helpers import Observable
from gajim.common.const import FTState
class FileTransfer(Observable):
_state_descriptions = {} # type: Dict[FTState, str]
def __init__(self, account):
Observable.__init__(self)
self._account = account
self._seen = 0
self.size = 0
self._state = None
self._error_text = ''
self._error_domain = None
@property
def account(self):
return self._account
@property
def state(self):
return self._state
@property
def seen(self):
return self._seen
@property
def is_complete(self):
if self.size == 0:
return False
return self._seen >= self.size
@property
def filename(self):
raise NotImplementedError
@property
def error_text(self):
return self._error_text
@property
def error_domain(self):
return self._error_domain
def get_state_description(self):
return self._state_descriptions.get(self._state, '')
def set_preparing(self):
self._state = FTState.PREPARING
self.notify('state-changed', FTState.PREPARING)
def set_encrypting(self):
self._state = FTState.ENCRYPTING
self.notify('state-changed', FTState.ENCRYPTING)
def set_decrypting(self):
self._state = FTState.DECRYPTING
self.notify('state-changed', FTState.DECRYPTING)
def set_started(self):
self._state = FTState.STARTED
self.notify('state-changed', FTState.STARTED)
def set_error(self, domain, text=''):
self._error_text = text
self._error_domain = domain
self._state = FTState.ERROR
self.notify('state-changed', FTState.ERROR)
self.disconnect_signals()
def set_cancelled(self):
self._state = FTState.CANCELLED
self.notify('state-changed', FTState.CANCELLED)
self.disconnect_signals()
def set_in_progress(self):
self._state = FTState.IN_PROGRESS
self.notify('state-changed', FTState.IN_PROGRESS)
def set_finished(self):
self._state = FTState.FINISHED
self.notify('state-changed', FTState.FINISHED)
self.disconnect_signals()
def update_progress(self):
self.notify('progress')
def cancel(self):
self.notify('cancel')

110
gajim/common/ged.py Normal file
View File

@ -0,0 +1,110 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
'''
Global Events Dispatcher module.
:author: Mateusz Biliński <mateusz@bilinski.it>
:since: 8th August 2008
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
:copyright: Copyright (2011) Yann Leboulanger <asterix@lagaule.org>
:license: GPL
'''
import logging
import traceback
import inspect
from nbxmpp import NodeProcessed
log = logging.getLogger('gajim.c.ged')
PRECORE = 10
CORE = 20
POSTCORE = 30
PREGUI = 40
PREGUI1 = 50
GUI1 = 60
POSTGUI1 = 70
PREGUI2 = 80
GUI2 = 90
POSTGUI2 = 100
POSTGUI = 110
OUT_PREGUI = 10
OUT_PREGUI1 = 20
OUT_GUI1 = 30
OUT_POSTGUI1 = 40
OUT_PREGUI2 = 50
OUT_GUI2 = 60
OUT_POSTGUI2 = 70
OUT_POSTGUI = 80
OUT_PRECORE = 90
OUT_CORE = 100
OUT_POSTCORE = 110
class GlobalEventsDispatcher:
def __init__(self):
self.handlers = {}
def register_event_handler(self, event_name, priority, handler):
if event_name in self.handlers:
handlers_list = self.handlers[event_name]
i = 0
for i, handler_tuple in enumerate(handlers_list):
if priority < handler_tuple[0]:
break
else:
# no event with smaller prio found, put it at the end
i += 1
handlers_list.insert(i, (priority, handler))
else:
self.handlers[event_name] = [(priority, handler)]
def remove_event_handler(self, event_name, priority, handler):
if event_name in self.handlers:
try:
self.handlers[event_name].remove((priority, handler))
except ValueError as error:
log.warning(
'''Function (%s) with priority "%s" never
registered as handler of event "%s". Couldn\'t remove.
Error: %s''', handler, priority, event_name, error)
def raise_event(self, event_name, *args, **kwargs):
log.debug('Raise event: %s', event_name)
if event_name in self.handlers:
node_processed = False
# Iterate over a copy of the handlers list, so while iterating
# the original handlers list can be modified
for _priority, handler in list(self.handlers[event_name]):
try:
if inspect.ismethod(handler):
log.debug('Call handler %s on %s',
handler.__name__,
handler.__self__)
else:
log.debug('Call handler %s', handler.__name__)
if handler(*args, **kwargs):
return True
except NodeProcessed:
node_processed = True
except Exception:
log.error('Error while running an event handler: %s',
handler)
traceback.print_exc()
if node_processed:
raise NodeProcessed

1442
gajim/common/helpers.py Normal file

File diff suppressed because it is too large Load Diff

194
gajim/common/i18n.py Normal file
View File

@ -0,0 +1,194 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2004 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2009 Benjamin Richter <br AT waldteufel-online.net>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import locale
import gettext
import unicodedata
from pathlib import Path
from gi.repository import GLib
DOMAIN = 'gajim'
LANG = 'en'
direction_mark = '\u200E'
_translation = gettext.NullTranslations()
def get_locale_dirs():
if os.name == 'nt':
return
path = gettext.find(DOMAIN)
if path is not None:
# gettext can find the location itself
# so we dont need the localedir
return
if Path('/app/share/run-as-flatpak').exists():
# Check if we run as flatpak
return [Path('/app/share/')]
data_dirs = [GLib.get_user_data_dir()] + GLib.get_system_data_dirs()
return [Path(dir_) for dir_ in data_dirs]
def iter_locale_dirs():
locale_dirs = get_locale_dirs()
if locale_dirs is None:
yield None
return
# gettext fallback
locale_dirs.append(Path(sys.base_prefix) / 'share')
found_paths = []
for path in locale_dirs:
locale_dir = path / 'locale'
if locale_dir in found_paths:
continue
found_paths.append(locale_dir)
if locale_dir.is_dir():
yield str(locale_dir)
def get_default_lang():
if os.name == "nt":
import ctypes
windll = ctypes.windll.kernel32
return locale.windows_locale[windll.GetUserDefaultUILanguage()]
if sys.platform == "darwin":
from AppKit import NSLocale
# FIXME: This returns a two letter language code (en, de, fr)
# We need a way to get en_US, de_DE etc.
return NSLocale.currentLocale().languageCode()
return locale.getdefaultlocale()[0] or 'en'
def get_rfc5646_lang(lang=None):
if lang is None:
lang = LANG
return lang.replace('_', '-')
def get_short_lang_code(lang=None):
if lang is None:
lang = LANG
return lang[:2]
def initialize_direction_mark():
from gi.repository import Gtk
global direction_mark
if Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL:
direction_mark = '\u200F'
def paragraph_direction_mark(text):
"""
Determine paragraph writing direction according to
http://www.unicode.org/reports/tr9/#The_Paragraph_Level
Returns either Unicode LTR mark or RTL mark.
"""
for char in text:
bidi = unicodedata.bidirectional(char)
if bidi == 'L':
return '\u200E'
if bidi in ('AL', 'R'):
return '\u200F'
return '\u200E'
def Q_(text):
"""
Translate the given text, optionally qualified with a special
construction, which will help translators to disambiguate between
same terms, but in different contexts.
When translated text is returned - this rudimentary construction
will be stripped off, if it's present.
Here is the construction to use:
Q_("?vcard:Unknown")
Everything between ? and : - is the qualifier to convey the context
to the translators. Everything after : - is the text itself.
"""
text = _(text)
if text.startswith('?'):
text = text.split(':', 1)[1]
return text
def ngettext(s_sing, s_plural, n, replace_sing=None, replace_plural=None):
"""
Use as:
i18n.ngettext(
'leave room %s', 'leave rooms %s', len(rooms), 'a', 'a, b, c')
In other words this is a hack to ngettext() to support %s %d etc..
"""
text = _translation.ngettext(s_sing, s_plural, n)
if n == 1 and replace_sing is not None:
text = text % replace_sing
elif n > 1 and replace_plural is not None:
text = text % replace_plural
return text
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error as error:
print(error, file=sys.stderr)
try:
LANG = get_default_lang()
if os.name == 'nt':
# Set the env var on Windows because gettext.find() uses it to
# find the translation
# Use LANGUAGE instead of LANG, LANG sets LC_ALL and thus
# doesn't retain other region settings like LC_TIME
os.environ['LANGUAGE'] = LANG
except Exception as error:
print('Failed to determine default language', file=sys.stderr)
import traceback
traceback.print_exc()
# Search for the translation in all locale dirs
for dir_ in iter_locale_dirs():
try:
_translation = gettext.translation(DOMAIN, dir_)
_ = _translation.gettext
if hasattr(locale, 'bindtextdomain'):
locale.bindtextdomain(DOMAIN, dir_) # type: ignore
except OSError:
continue
else:
break
else:
print('No translations found', file=sys.stderr)
print('Dirs searched: %s' % get_locale_dirs(), file=sys.stderr)
_ = _translation.gettext

362
gajim/common/idle.py Normal file
View File

@ -0,0 +1,362 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Mateusz Biliński <mateusz AT bilinski.it>
# Copyright (C) 2008 Thorsten P. 'dGhvcnN0ZW5wIEFUIHltYWlsIGNvbQ==\n'.decode("base64")
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import sys
import time
import ctypes
import ctypes.util
import logging
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import GObject
from gajim.common import app
from gajim.common.const import Display
from gajim.common.const import IdleState
log = logging.getLogger('gajim.c.idle')
class DBusFreedesktopIdleMonitor:
def __init__(self):
self.last_idle_time = 0
self._extended_away = False
log.debug('Connecting to D-Bus')
self.dbus_proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
'org.freedesktop.ScreenSaver',
'/org/freedesktop/ScreenSaver',
'org.freedesktop.ScreenSaver',
None
)
log.debug('D-Bus connected')
# Only the following call will trigger exceptions if the D-Bus
# interface/method/... does not exist. Using the failing method
# for class init to allow other idle monitors to be used on failure.
self._get_idle_sec_fail()
log.debug('D-Bus call test successful')
def _get_idle_sec_fail(self):
(idle_time,) = self.dbus_proxy.call_sync(
'GetSessionIdleTime',
None,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
None
)
return idle_time//1000
def get_idle_sec(self):
try:
self.last_idle_time = self._get_idle_sec_fail()
except GLib.Error as error:
log.warning(
'org.freedesktop.ScreenSaver.GetSessionIdleTime() failed: %s',
error)
return self.last_idle_time
def set_extended_away(self, state):
self._extended_away = state
def is_extended_away(self):
return self._extended_away
class DBusGnomeIdleMonitor:
def __init__(self):
self.last_idle_time = 0
self._extended_away = False
log.debug('Connecting to D-Bus')
self.dbus_gnome_proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
'org.gnome.Mutter.IdleMonitor',
'/org/gnome/Mutter/IdleMonitor/Core',
'org.gnome.Mutter.IdleMonitor',
None
)
log.debug('D-Bus connected')
# Only the following call will trigger exceptions if the D-Bus
# interface/method/... does not exist. Using the failing method
# for class init to allow other idle monitors to be used on failure.
self._get_idle_sec_fail()
log.debug('D-Bus call test successful')
def _get_idle_sec_fail(self):
(idle_time,) = self.dbus_gnome_proxy.call_sync(
'GetIdletime',
None,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
None
)
return int(idle_time / 1000)
def get_idle_sec(self):
try:
self.last_idle_time = self._get_idle_sec_fail()
except GLib.Error as error:
log.warning(
'org.gnome.Mutter.IdleMonitor.GetIdletime() failed: %s',
error)
return self.last_idle_time
def set_extended_away(self, state):
self._extended_away = state
def is_extended_away(self):
return self._extended_away
class XssIdleMonitor:
def __init__(self):
self._extended_away = False
class XScreenSaverInfo(ctypes.Structure):
_fields_ = [
('window', ctypes.c_ulong),
('state', ctypes.c_int),
('kind', ctypes.c_int),
('til_or_since', ctypes.c_ulong),
('idle', ctypes.c_ulong),
('eventMask', ctypes.c_ulong)
]
XScreenSaverInfo_p = ctypes.POINTER(XScreenSaverInfo)
display_p = ctypes.c_void_p
xid = ctypes.c_ulong
c_int_p = ctypes.POINTER(ctypes.c_int)
libX11path = ctypes.util.find_library('X11')
if libX11path is None:
raise OSError('libX11 could not be found.')
libX11 = ctypes.cdll.LoadLibrary(libX11path)
libX11.XOpenDisplay.restype = display_p
libX11.XOpenDisplay.argtypes = (ctypes.c_char_p,)
libX11.XDefaultRootWindow.restype = xid
libX11.XDefaultRootWindow.argtypes = (display_p,)
libXsspath = ctypes.util.find_library('Xss')
if libXsspath is None:
raise OSError('libXss could not be found.')
self.libXss = ctypes.cdll.LoadLibrary(libXsspath)
self.libXss.XScreenSaverQueryExtension.argtypes = display_p, c_int_p, c_int_p
self.libXss.XScreenSaverAllocInfo.restype = XScreenSaverInfo_p
self.libXss.XScreenSaverQueryInfo.argtypes = (
display_p, xid, XScreenSaverInfo_p)
self.dpy_p = libX11.XOpenDisplay(None)
if self.dpy_p is None:
raise OSError('Could not open X Display.')
_event_basep = ctypes.c_int()
_error_basep = ctypes.c_int()
extension = self.libXss.XScreenSaverQueryExtension(
self.dpy_p, ctypes.byref(_event_basep), ctypes.byref(_error_basep))
if extension == 0:
raise OSError('XScreenSaver Extension not available on display.')
self.xss_info_p = self.libXss.XScreenSaverAllocInfo()
if self.xss_info_p is None:
raise OSError('XScreenSaverAllocInfo: Out of Memory.')
self.rootwindow = libX11.XDefaultRootWindow(self.dpy_p)
def get_idle_sec(self):
info = self.libXss.XScreenSaverQueryInfo(
self.dpy_p, self.rootwindow, self.xss_info_p)
if info == 0:
return info
return int(self.xss_info_p.contents.idle / 1000)
def set_extended_away(self, state):
self._extended_away = state
def is_extended_away(self):
return False
class WindowsIdleMonitor:
def __init__(self):
self.OpenInputDesktop = ctypes.windll.user32.OpenInputDesktop
self.CloseDesktop = ctypes.windll.user32.CloseDesktop
self.SystemParametersInfo = ctypes.windll.user32.SystemParametersInfoW
self.GetTickCount = ctypes.windll.kernel32.GetTickCount
self.GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo
self._locked_time = None
class LASTINPUTINFO(ctypes.Structure):
_fields_ = [('cbSize', ctypes.c_uint), ('dwTime', ctypes.c_uint)]
self.lastInputInfo = LASTINPUTINFO()
self.lastInputInfo.cbSize = ctypes.sizeof(self.lastInputInfo)
def get_idle_sec(self):
self.GetLastInputInfo(ctypes.byref(self.lastInputInfo))
return float(self.GetTickCount() - self.lastInputInfo.dwTime) / 1000
def is_extended_away(self):
# Check if Screen Saver is running
# 0x72 is SPI_GETSCREENSAVERRUNNING
saver_runing = ctypes.c_int(0)
info = self.SystemParametersInfo(
0x72, 0, ctypes.byref(saver_runing), 0)
if info and saver_runing.value:
return True
# Check if Screen is locked
# Also a UAC prompt counts as locked
# So just return True if we are more than 10 seconds locked
desk = self.OpenInputDesktop(0, False, 0)
unlocked = bool(desk)
self.CloseDesktop(desk)
if unlocked:
self._locked_time = None
return False
if self._locked_time is None:
self._locked_time = time.time()
return False
threshold = time.time() - 10
if threshold > self._locked_time:
return True
class IdleMonitor(GObject.GObject):
__gsignals__ = {
'state-changed': (
GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
None, # return value
() # arguments
)}
def __init__(self):
GObject.GObject.__init__(self)
self.set_interval()
self._state = IdleState.AWAKE
self._idle_monitor = self._get_idle_monitor()
if self.is_available():
GLib.timeout_add_seconds(1, self._poll)
def set_interval(self, away_interval=60, xa_interval=120):
log.info('Set interval: away: %s, xa: %s',
away_interval, xa_interval)
self._away_interval = away_interval
self._xa_interval = xa_interval
def set_extended_away(self, state):
self._idle_monitor.set_extended_away(state)
def is_available(self):
return self._idle_monitor is not None
@property
def state(self):
if not self.is_available():
return IdleState.UNKNOWN
return self._state
def is_xa(self):
return self.state == IdleState.XA
def is_away(self):
return self.state == IdleState.AWAY
def is_awake(self):
return self.state == IdleState.AWAKE
def is_unknown(self):
return self.state == IdleState.UNKNOWN
@staticmethod
def _get_idle_monitor():
if sys.platform == 'win32':
return WindowsIdleMonitor()
try:
return DBusFreedesktopIdleMonitor()
except GLib.Error as error:
log.info('Idle time via D-Bus not available: %s', error)
try:
return DBusGnomeIdleMonitor()
except GLib.Error as error:
log.info('Idle time via D-Bus (GNOME) not available: %s', error)
if app.is_display(Display.WAYLAND):
return
try:
return XssIdleMonitor()
except OSError as error:
log.info('Idle time via XScreenSaverInfo not available: %s', error)
def get_idle_sec(self):
return self._idle_monitor.get_idle_sec()
def _poll(self):
"""
Check to see if we should change state
"""
if self._idle_monitor.is_extended_away():
log.info('Extended Away: Screensaver or Locked Screen')
self._set_state(IdleState.XA)
return True
idle_time = self.get_idle_sec()
# xa is stronger than away so check for xa first
if idle_time > self._xa_interval:
self._set_state(IdleState.XA)
elif idle_time > self._away_interval:
self._set_state(IdleState.AWAY)
else:
self._set_state(IdleState.AWAKE)
return True
def _set_state(self, state):
if self._state == state:
return
self._state = state
log.info('State changed: %s', state)
self.emit('state-changed')
Monitor = IdleMonitor()

View File

@ -0,0 +1,245 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
"""
Handles Jingle contents (XEP 0166)
"""
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import configpaths
from gajim.common.jingle_xtls import SELF_SIGNED_CERTIFICATE
from gajim.common.jingle_xtls import load_cert_file
contents = {} # type: Dict[str, Any]
def get_jingle_content(node):
namespace = node.getNamespace()
if namespace in contents:
return contents[namespace](node)
class JingleContentSetupException(Exception):
"""
Exception that should be raised when a content fails to setup.
"""
class JingleContent:
"""
An abstraction of content in Jingle sessions
"""
def __init__(self, session, transport, senders):
self.session = session
self.transport = transport
# will be filled by JingleSession.add_content()
# don't uncomment these lines, we will catch more buggy code then
# (a JingleContent not added to session shouldn't send anything)
self.creator = None
self.name = None
self.accepted = False
self.sent = False
self.negotiated = False
self.media = None
self.senders = senders
if self.senders is None:
self.senders = 'both'
self.allow_sending = True # Used for stream direction, attribute 'senders'
# These were found by the Politie
self.file_props = None
self.use_security = None
self.callbacks = {
# these are called when *we* get stanzas
'content-accept': [self.__on_transport_info,
self.__on_content_accept],
'content-add': [self.__on_transport_info],
'content-modify': [],
'content-reject': [],
'content-remove': [],
'description-info': [],
'security-info': [],
'session-accept': [self.__on_transport_info,
self.__on_content_accept],
'session-info': [],
'session-initiate': [self.__on_transport_info],
'session-terminate': [],
'transport-info': [self.__on_transport_info],
'transport-replace': [self.__on_transport_replace],
'transport-accept': [self.__on_transport_replace],
'transport-reject': [],
'iq-result': [],
'iq-error': [],
# these are called when *we* sent these stanzas
'content-accept-sent': [self.__fill_jingle_stanza,
self.__on_content_accept],
'content-add-sent': [self.__fill_jingle_stanza],
'session-initiate-sent': [self.__fill_jingle_stanza],
'session-accept-sent': [self.__fill_jingle_stanza,
self.__on_content_accept],
'session-terminate-sent': [],
}
def is_ready(self):
return self.accepted and not self.sent
def __on_content_accept(self, stanza, content, error, action):
self.on_negotiated()
def on_negotiated(self):
if self.accepted:
self.negotiated = True
self.session.content_negotiated(self.media)
def add_remote_candidates(self, candidates):
"""
Add a list of candidates to the list of remote candidates
"""
self.transport.remote_candidates = candidates
def on_stanza(self, stanza, content, error, action):
"""
Called when something related to our content was sent by peer
"""
if action in self.callbacks:
for callback in self.callbacks[action]:
callback(stanza, content, error, action)
def __on_transport_replace(self, stanza, content, error, action):
content.addChild(node=self.transport.make_transport())
def __on_transport_info(self, stanza, content, error, action):
"""
Got a new transport candidate
"""
candidates = self.transport.parse_transport_stanza(
content.getTag('transport'))
if candidates:
self.add_remote_candidates(candidates)
def __content(self, payload=None):
"""
Build a XML content-wrapper for our data
"""
if payload is None:
payload = []
return nbxmpp.Node('content',
attrs={'name': self.name,
'creator': self.creator,
'senders': self.senders},
payload=payload)
def send_candidate(self, candidate):
"""
Send a transport candidate for a previously defined transport.
"""
content = self.__content()
content.addChild(node=self.transport.make_transport([candidate]))
self.session.send_transport_info(content)
def send_error_candidate(self):
"""
Sends a candidate-error when we can't connect to a candidate.
"""
content = self.__content()
tp = self.transport.make_transport(add_candidates=False)
tp.addChild(name='candidate-error')
content.addChild(node=tp)
self.session.send_transport_info(content)
def send_description_info(self):
content = self.__content()
self._fill_content(content)
self.session.send_description_info(content)
def __fill_jingle_stanza(self, stanza, content, error, action):
"""
Add our things to session-initiate stanza
"""
self._fill_content(content)
self.sent = True
content.addChild(node=self.transport.make_transport())
def _fill_content(self, content):
description_node = nbxmpp.simplexml.Node(
tag=Namespace.JINGLE_FILE_TRANSFER_5 + ' description')
file_tag = description_node.setTag('file')
if self.file_props.name:
node = nbxmpp.simplexml.Node(tag='name')
node.addData(self.file_props.name)
file_tag.addChild(node=node)
if self.file_props.date:
node = nbxmpp.simplexml.Node(tag='date')
node.addData(self.file_props.date)
file_tag.addChild(node=node)
if self.file_props.size:
node = nbxmpp.simplexml.Node(tag='size')
node.addData(self.file_props.size)
file_tag.addChild(node=node)
if self.file_props.type_ == 'r':
if self.file_props.hash_:
file_tag.addChild('hash', attrs={'algo': self.file_props.algo},
namespace=Namespace.HASHES_2,
payload=self.file_props.hash_)
else:
# if the file is less than 10 mb, then it is small
# lets calculate it right away
if self.file_props.size < 10000000 and not self.file_props.hash_:
hash_data = self._compute_hash()
if hash_data:
file_tag.addChild(node=hash_data)
pjid = app.get_jid_without_resource(self.session.peerjid)
file_info = {'name' : self.file_props.name,
'file-name' : self.file_props.file_name,
'hash' : self.file_props.hash_,
'size' : self.file_props.size,
'date' : self.file_props.date,
'peerjid' : pjid
}
self.session.connection.get_module('Jingle').set_file_info(file_info)
desc = file_tag.setTag('desc')
if self.file_props.desc:
desc.setData(self.file_props.desc)
if self.use_security:
security = nbxmpp.simplexml.Node(
tag=Namespace.JINGLE_XTLS + ' security')
certpath = configpaths.get('MY_CERT') / (SELF_SIGNED_CERTIFICATE
+ '.cert')
cert = load_cert_file(certpath)
if cert:
digest_algo = (cert.get_signature_algorithm()
.decode('utf-8').split('With')[0])
security.addChild('fingerprint').addData(cert.digest(
digest_algo).decode('utf-8'))
for m in ('x509', ): # supported authentication methods
method = nbxmpp.simplexml.Node(tag='method')
method.setAttr('name', m)
security.addChild(node=method)
content.addChild(node=security)
content.addChild(node=description_node)
def destroy(self):
self.callbacks = None
del self.session.contents[(self.creator, self.name)]

409
gajim/common/jingle_ft.py Normal file
View File

@ -0,0 +1,409 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
"""
Handles Jingle File Transfer (XEP 0234)
"""
import hashlib
import logging
import threading
from enum import IntEnum, unique
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import configpaths
from gajim.common import jingle_xtls
from gajim.common.jingle_content import contents, JingleContent
from gajim.common.jingle_transport import JingleTransportSocks5, TransportType
from gajim.common import helpers
from gajim.common.connection_handlers_events import FileRequestReceivedEvent
from gajim.common.jingle_ftstates import (
StateInitialized, StateCandSent, StateCandReceived, StateTransfering,
StateCandSentAndRecv, StateTransportReplace)
log = logging.getLogger('gajim.c.jingle_ft')
@unique
class State(IntEnum):
NOT_STARTED = 0
INITIALIZED = 1
# We send the candidates and we are waiting for a reply
CAND_SENT = 2
# We received the candidates and we are waiting to reply
CAND_RECEIVED = 3
# We have sent and received the candidates
# This also includes any candidate-error received or sent
CAND_SENT_AND_RECEIVED = 4
TRANSPORT_REPLACE = 5
# We are transferring the file
TRANSFERRING = 6
class JingleFileTransfer(JingleContent):
def __init__(self, session, transport=None, file_props=None,
use_security=False, senders=None):
JingleContent.__init__(self, session, transport, senders)
log.info("transport value: %s", transport)
# events we might be interested in
self.callbacks['session-initiate'] += [self.__on_session_initiate]
self.callbacks['session-initiate-sent'] += [
self.__on_session_initiate_sent]
self.callbacks['content-add'] += [self.__on_session_initiate]
self.callbacks['session-accept'] += [self.__on_session_accept]
self.callbacks['session-terminate'] += [self.__on_session_terminate]
self.callbacks['session-info'] += [self.__on_session_info]
self.callbacks['transport-accept'] += [self.__on_transport_accept]
self.callbacks['transport-replace'] += [self.__on_transport_replace]
self.callbacks['session-accept-sent'] += [self.__transport_setup]
# fallback transport method
self.callbacks['transport-reject'] += [self.__on_transport_reject]
self.callbacks['transport-info'] += [self.__on_transport_info]
self.callbacks['iq-result'] += [self.__on_iq_result]
self.use_security = use_security
self.x509_fingerprint = None
self.file_props = file_props
self.weinitiate = self.session.weinitiate
self.werequest = self.session.werequest
if self.file_props is not None:
if self.session.werequest:
self.file_props.sender = self.session.peerjid
self.file_props.receiver = self.session.ourjid
else:
self.file_props.sender = self.session.ourjid
self.file_props.receiver = self.session.peerjid
self.file_props.session_type = 'jingle'
self.file_props.sid = session.sid
self.file_props.transfered_size = []
self.file_props.transport_sid = self.transport.sid
log.info("FT request: %s", file_props)
if transport is None:
self.transport = JingleTransportSocks5()
self.transport.set_connection(session.connection)
self.transport.set_file_props(self.file_props)
self.transport.set_our_jid(session.ourjid)
log.info('ourjid: %s', session.ourjid)
self.session = session
self.media = 'file'
self.nominated_cand = {}
if app.contacts.is_gc_contact(session.connection.name,
session.peerjid):
roomjid = session.peerjid.split('/')[0]
dstaddr = hashlib.sha1(('%s%s%s' % (self.file_props.sid,
session.ourjid, roomjid))
.encode('utf-8')).hexdigest()
self.file_props.dstaddr = dstaddr
self.state = State.NOT_STARTED
self.states = {
State.INITIALIZED : StateInitialized(self),
State.CAND_SENT : StateCandSent(self),
State.CAND_RECEIVED : StateCandReceived(self),
State.TRANSFERRING : StateTransfering(self),
State.TRANSPORT_REPLACE : StateTransportReplace(self),
State.CAND_SENT_AND_RECEIVED : StateCandSentAndRecv(self)
}
cert_name = (configpaths.get('MY_CERT') /
jingle_xtls.SELF_SIGNED_CERTIFICATE)
if not (cert_name.with_suffix('.cert').exists()
and cert_name.with_suffix('.pkey').exists()):
jingle_xtls.make_certs(cert_name, 'gajim')
def __state_changed(self, nextstate, args=None):
# Executes the next state action and sets the next state
current_state = self.state
st = self.states[nextstate]
st.action(args)
# state can have been changed during the action. Don't go back.
if self.state == current_state:
self.state = nextstate
def __on_session_initiate(self, stanza, content, error, action):
log.debug("Jingle FT request received")
app.nec.push_incoming_event(FileRequestReceivedEvent(None,
conn=self.session.connection,
stanza=stanza,
jingle_content=content,
FT_content=self))
if self.session.request:
# accept the request
self.session.approve_content(self.media, self.name)
self.session.accept_session()
def __on_session_initiate_sent(self, stanza, content, error, action):
pass
def __send_hash(self):
# Send hash in a session info
checksum = nbxmpp.Node(tag='checksum',
payload=[nbxmpp.Node(tag='file',
payload=[self._compute_hash()])])
checksum.setNamespace(Namespace.JINGLE_FILE_TRANSFER_5)
self.session.__session_info(checksum)
pjid = app.get_jid_without_resource(self.session.peerjid)
file_info = {'name' : self.file_props.name,
'file-name' : self.file_props.file_name,
'hash' : self.file_props.hash_,
'size' : self.file_props.size,
'date' : self.file_props.date,
'peerjid' : pjid
}
self.session.connection.get_module('Jingle').set_file_info(file_info)
def _compute_hash(self):
# Calculates the hash and returns a xep-300 hash stanza
if self.file_props.algo is None:
return
try:
file_ = open(self.file_props.file_name, 'rb')
except IOError:
# can't open file
return
h = nbxmpp.Hashes2()
hash_ = h.calculateHash(self.file_props.algo, file_)
file_.close()
# DEBUG
#hash_ = '1294809248109223'
if not hash_:
# Hash algorithm not supported
return
self.file_props.hash_ = hash_
h.addHash(hash_, self.file_props.algo)
return h
def on_cert_received(self):
self.session.approve_session()
self.session.approve_content('file', name=self.name)
def __on_session_accept(self, stanza, content, error, action):
log.info("__on_session_accept")
con = self.session.connection
security = content.getTag('security')
if not security: # responder can not verify our fingerprint
self.use_security = False
else:
fingerprint = security.getTag('fingerprint')
if fingerprint:
fingerprint = fingerprint.getData()
self.x509_fingerprint = fingerprint
if not jingle_xtls.check_cert(app.get_jid_without_resource(
self.session.responder), fingerprint):
id_ = jingle_xtls.send_cert_request(con,
self.session.responder)
jingle_xtls.key_exchange_pend(id_,
self.continue_session_accept,
[stanza])
raise nbxmpp.NodeProcessed
self.continue_session_accept(stanza)
def continue_session_accept(self, stanza):
if self.state == State.TRANSPORT_REPLACE:
# If we are requesting we don't have the file
if self.session.werequest:
raise nbxmpp.NodeProcessed
# We send the file
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
self.file_props.streamhosts = self.transport.remote_candidates
# Calculate file hash in a new thread
# if we haven't sent the hash already.
if self.file_props.hash_ is None and self.file_props.algo and \
not self.werequest:
self.hash_thread = threading.Thread(target=self.__send_hash)
self.hash_thread.start()
for host in self.file_props.streamhosts:
host['initiator'] = self.session.initiator
host['target'] = self.session.responder
host['sid'] = self.file_props.sid
fingerprint = None
if self.use_security:
fingerprint = 'client'
if self.transport.type_ == TransportType.SOCKS5:
sid = self.file_props.transport_sid
app.socks5queue.connect_to_hosts(self.session.connection.name,
sid,
self.on_connect,
self._on_connect_error,
fingerprint=fingerprint,
receiving=False)
raise nbxmpp.NodeProcessed
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
def __on_session_terminate(self, stanza, content, error, action):
log.info("__on_session_terminate")
def __on_session_info(self, stanza, content, error, action):
pass
def __on_transport_accept(self, stanza, content, error, action):
log.info("__on_transport_accept")
def __on_transport_replace(self, stanza, content, error, action):
log.info("__on_transport_replace")
def __on_transport_reject(self, stanza, content, error, action):
log.info("__on_transport_reject")
def __on_transport_info(self, stanza, content, error, action):
log.info("__on_transport_info")
cand_error = content.getTag('transport').getTag('candidate-error')
cand_used = content.getTag('transport').getTag('candidate-used')
if (cand_error or cand_used) and \
self.state >= State.CAND_SENT_AND_RECEIVED:
raise nbxmpp.NodeProcessed
if cand_error:
if not app.socks5queue.listener.connections:
app.socks5queue.listener.disconnect()
self.nominated_cand['peer-cand'] = False
if self.state == State.CAND_SENT:
if not self.nominated_cand['our-cand'] and \
not self.nominated_cand['peer-cand']:
if not self.weinitiate:
return
self.__state_changed(State.TRANSPORT_REPLACE)
else:
response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.session.connection.connection.send(response)
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
else:
args = {'candError' : True}
self.__state_changed(State.CAND_RECEIVED, args)
return
if cand_used:
streamhost_cid = cand_used.getAttr('cid')
streamhost_used = None
for cand in self.transport.candidates:
if cand['candidate_id'] == streamhost_cid:
streamhost_used = cand
break
if streamhost_used is None or streamhost_used['type'] == 'proxy':
if app.socks5queue.listener and \
not app.socks5queue.listener.connections:
app.socks5queue.listener.disconnect()
if content.getTag('transport').getTag('activated'):
self.state = State.TRANSFERRING
app.socks5queue.send_file(self.file_props,
self.session.connection.name, 'client')
return
args = {'content': content,
'sendCand': False}
if self.state == State.CAND_SENT:
self.__state_changed(State.CAND_SENT_AND_RECEIVED, args)
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
self.__state_changed(State.CAND_RECEIVED, args)
def __on_iq_result(self, stanza, content, error, action):
log.info("__on_iq_result")
if self.state in (State.NOT_STARTED, State.CAND_RECEIVED):
self.__state_changed(State.INITIALIZED)
elif self.state == State.CAND_SENT_AND_RECEIVED:
if not self.nominated_cand['our-cand'] and \
not self.nominated_cand['peer-cand']:
if not self.weinitiate:
return
self.__state_changed(State.TRANSPORT_REPLACE)
return
# initiate transfer
self.__state_changed(State.TRANSFERRING)
def __transport_setup(self, stanza=None, content=None, error=None,
action=None):
# Sets up a few transport specific things for the file transfer
if self.transport.type_ == TransportType.IBB:
# No action required, just set the state to transferring
self.state = State.TRANSFERRING
else:
self._listen_host()
def on_connect(self, streamhost):
"""
send candidate-used stanza
"""
log.info('send_candidate_used')
if streamhost is None:
return
args = {'streamhost' : streamhost,
'sendCand' : True}
self.nominated_cand['our-cand'] = streamhost
self.__send_candidate(args)
def _on_connect_error(self, sid):
log.info('connect error, sid=%s', sid)
args = {'candError' : True,
'sendCand' : True}
self.__send_candidate(args)
def __send_candidate(self, args):
if self.state == State.CAND_RECEIVED:
self.__state_changed(State.CAND_SENT_AND_RECEIVED, args)
else:
self.__state_changed(State.CAND_SENT, args)
def _store_socks5_sid(self, sid, hash_id):
# callback from socsk5queue.start_listener
self.file_props.hash_ = hash_id
def _listen_host(self):
receiver = self.file_props.receiver
sender = self.file_props.sender
sha_str = helpers.get_auth_sha(self.file_props.sid, sender,
receiver)
self.file_props.sha_str = sha_str
port = app.settings.get('file_transfers_port')
fingerprint = None
if self.use_security:
fingerprint = 'server'
listener = app.socks5queue.start_listener(port, sha_str,
self._store_socks5_sid,
self.file_props,
fingerprint=fingerprint,
typ='sender' if self.weinitiate else 'receiver')
if not listener:
# send error message, notify the user
return
def is_our_candidate_used(self):
'''
If this method returns true then the candidate we nominated will be
used, if false, the candidate nominated by peer will be used
'''
if not self.nominated_cand['peer-cand']:
return True
if not self.nominated_cand['our-cand']:
return False
peer_pr = int(self.nominated_cand['peer-cand']['priority'])
our_pr = int(self.nominated_cand['our-cand']['priority'])
if peer_pr != our_pr:
return our_pr > peer_pr
return self.weinitiate
def start_ibb_transfer(self):
if self.file_props.type_ == 's':
self.__state_changed(State.TRANSFERRING)
def get_content(desc):
return JingleFileTransfer
contents[Namespace.JINGLE_FILE_TRANSFER_5] = get_content

View File

@ -0,0 +1,228 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.jingle_transport import TransportType
from gajim.common.socks5 import Socks5ReceiverClient
from gajim.common.socks5 import Socks5SenderClient
log = logging.getLogger('gajim.c.jingle_ftstates')
class JingleFileTransferStates:
'''
This class implements the state machine design pattern
'''
def __init__(self, jingleft):
self.jft = jingleft
def action(self, args=None):
'''
This method MUST be overridden by a subclass
'''
raise NotImplementedError('This is an abstract method!')
class StateInitialized(JingleFileTransferStates):
'''
This state initializes the file transfer
'''
def action(self, args=None):
if self.jft.weinitiate:
# update connection's fileprops
self.jft._listen_host()
# Listen on configured port for file transfer
else:
fingerprint = None
if self.jft.use_security:
fingerprint = 'client'
# Connect to the candidate host, on success call on_connect method
app.socks5queue.connect_to_hosts(self.jft.session.connection.name,
self.jft.file_props.transport_sid, self.jft.on_connect,
self.jft._on_connect_error, fingerprint=fingerprint)
class StateCandSent(JingleFileTransferStates):
'''
This state sends our nominated candidate
'''
def _send_candidate(self, args):
if 'candError' in args:
self.jft.nominated_cand['our-cand'] = False
self.jft.send_error_candidate()
return
# Send candidate used
streamhost = args['streamhost']
self.jft.nominated_cand['our-cand'] = streamhost
content = nbxmpp.Node('content')
content.setAttr('creator', 'initiator')
content.setAttr('name', self.jft.name)
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
transport.setAttr('sid', self.jft.transport.sid)
candidateused = nbxmpp.Node('candidate-used')
candidateused.setAttr('cid', streamhost['candidate_id'])
transport.addChild(node=candidateused)
content.addChild(node=transport)
self.jft.session.send_transport_info(content)
def action(self, args=None):
self._send_candidate(args)
class StateCandReceived(JingleFileTransferStates):
'''
This state happens when we receive a candidate.
It takes the arguments: canError if we receive a candidate-error
'''
def _recv_candidate(self, args):
if 'candError' in args:
return
content = args['content']
streamhost_cid = content.getTag('transport').getTag('candidate-used').\
getAttr('cid')
streamhost_used = None
for cand in self.jft.transport.candidates:
if cand['candidate_id'] == streamhost_cid:
streamhost_used = cand
break
if streamhost_used is None:
log.info("unknown streamhost")
return
# We save the candidate nominated by peer
self.jft.nominated_cand['peer-cand'] = streamhost_used
def action(self, args=None):
self._recv_candidate(args)
class StateCandSentAndRecv(StateCandSent, StateCandReceived):
'''
This state happens when we have received and sent the candidates.
It takes the boolean argument: sendCand in order to decide whether
we should execute the action of when we receive or send a candidate.
'''
def action(self, args=None):
if args['sendCand']:
self._send_candidate(args)
else:
self._recv_candidate(args)
class StateTransportReplace(JingleFileTransferStates):
'''
This state initiates transport replace
'''
def action(self, args=None):
self.jft.session.transport_replace()
class StateTransfering(JingleFileTransferStates):
'''
This state will start the transfer depending on the type of transport
we have.
'''
def _start_ibb_transfer(self, con):
self.jft.file_props.transport_sid = self.jft.transport.sid
fp = open(self.jft.file_props.file_name, 'rb')
con.get_module('IBB').send_open(self.jft.session.peerjid,
self.jft.file_props.sid,
fp)
def _start_sock5_transfer(self):
# It tells whether we start the transfer as client or server
mode = None
if self.jft.is_our_candidate_used():
mode = 'client'
streamhost_used = self.jft.nominated_cand['our-cand']
app.socks5queue.remove_server(self.jft.file_props.transport_sid)
else:
mode = 'server'
streamhost_used = self.jft.nominated_cand['peer-cand']
app.socks5queue.remove_client(self.jft.file_props.transport_sid)
app.socks5queue.remove_other_servers(streamhost_used['host'])
if streamhost_used['type'] == 'proxy':
self.jft.file_props.is_a_proxy = True
if self.jft.file_props.type_ == 's' and self.jft.weinitiate:
self.jft.file_props.proxy_sender = streamhost_used['initiator']
self.jft.file_props.proxy_receiver = streamhost_used['target']
else:
self.jft.file_props.proxy_sender = streamhost_used['target']
self.jft.file_props.proxy_receiver = streamhost_used[
'initiator']
if self.jft.file_props.type_ == 's':
s = app.socks5queue.senders
for sender in s:
if s[sender].host == streamhost_used['host'] and \
s[sender].connected:
return
elif self.jft.file_props.type_ == 'r':
r = app.socks5queue.readers
for reader in r:
if r[reader].host == streamhost_used['host'] and \
r[reader].connected:
return
else:
raise TypeError
self.jft.file_props.streamhost_used = True
streamhost_used['sid'] = self.jft.file_props.transport_sid
self.jft.file_props.streamhosts = []
self.jft.file_props.streamhosts.append(streamhost_used)
self.jft.file_props.proxyhosts = []
self.jft.file_props.proxyhosts.append(streamhost_used)
if self.jft.file_props.type_ == 's':
app.socks5queue.idx += 1
idx = app.socks5queue.idx
sockobj = Socks5SenderClient(app.idlequeue, idx,
app.socks5queue, _sock=None,
host=str(streamhost_used['host']),
port=int(streamhost_used['port']),
fingerprint=None, connected=False,
file_props=self.jft.file_props)
else:
sockobj = Socks5ReceiverClient(app.idlequeue, streamhost_used,
transport_sid=self.jft.file_props.transport_sid,
file_props=self.jft.file_props, fingerprint=None)
sockobj.proxy = True
sockobj.streamhost = streamhost_used
app.socks5queue.add_sockobj(self.jft.session.connection.name,
sockobj)
streamhost_used['idx'] = sockobj.queue_idx
# If we offered the nominated candidate used, we activate
# the proxy
if not self.jft.is_our_candidate_used():
app.socks5queue.on_success[self.jft.file_props.transport_sid]\
= self.jft.transport._on_proxy_auth_ok
# TODO: add on failure
else:
app.socks5queue.send_file(self.jft.file_props,
self.jft.session.connection.name, mode)
def action(self, args=None):
if self.jft.transport.type_ == TransportType.IBB:
self._start_ibb_transfer(self.jft.session.connection)
elif self.jft.transport.type_ == TransportType.SOCKS5:
self._start_sock5_transfer()

534
gajim/common/jingle_rtp.py Normal file
View File

@ -0,0 +1,534 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
"""
Handles Jingle RTP sessions (XEP 0167)
"""
import os
import logging
import socket
from collections import deque
from datetime import datetime
import nbxmpp
from nbxmpp.namespaces import Namespace
from gi.repository import GLib
try:
from gi.repository import Farstream
from gi.repository import Gst
except Exception:
pass
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.jingle_transport import JingleTransportICEUDP
from gajim.common.jingle_content import contents
from gajim.common.jingle_content import JingleContent
from gajim.common.jingle_content import JingleContentSetupException
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.jingle_session import FailedApplication
log = logging.getLogger('gajim.c.jingle_rtp')
class JingleRTPContent(JingleContent):
def __init__(self, session, media, transport=None):
if transport is None:
transport = JingleTransportICEUDP(None)
JingleContent.__init__(self, session, transport, None)
self.media = media
self._dtmf_running = False
self.farstream_media = {
'audio': Farstream.MediaType.AUDIO,
'video': Farstream.MediaType.VIDEO}[media]
self.pipeline = None
self.src_bin = None
self.stream_failed_once = False
self.candidates_ready = False # True when local candidates are prepared
# TODO
self.conference = None
self.funnel = None
self.p2psession = None
self.p2pstream = None
self.callbacks['session-initiate'] += [self.__on_remote_codecs]
self.callbacks['content-add'] += [self.__on_remote_codecs]
self.callbacks['description-info'] += [self.__on_remote_codecs]
self.callbacks['content-accept'] += [self.__on_remote_codecs]
self.callbacks['session-accept'] += [self.__on_remote_codecs]
self.callbacks['session-terminate'] += [self.__stop]
self.callbacks['session-terminate-sent'] += [self.__stop]
def setup_stream(self, on_src_pad_added):
# pipeline and bus
self.pipeline = Gst.Pipeline()
bus = self.pipeline.get_bus()
bus.add_signal_watch()
bus.connect('message', self._on_gst_message)
# conference
self.conference = Gst.ElementFactory.make('fsrtpconference', None)
self.pipeline.add(self.conference)
self.funnel = None
self.p2psession = self.conference.new_session(self.farstream_media)
participant = self.conference.new_participant()
# FIXME: Consider a workaround, here...
# pidgin and telepathy-gabble don't follow the XEP, and it won't work
# due to bad controlling-mode
params = {'controlling-mode': self.session.weinitiate, 'debug': False}
if app.settings.get('use_stun_server'):
stun_server = app.settings.get('stun_server')
if not stun_server and self.session.connection._stun_servers:
stun_server = self.session.connection._stun_servers[0]['host']
if stun_server:
try:
ip = socket.getaddrinfo(stun_server, 0, socket.AF_UNSPEC,
socket.SOCK_STREAM)[0][4][0]
except socket.gaierror as e:
log.warning('Lookup of stun ip failed: %s', str(e))
else:
params['stun-ip'] = ip
self.p2pstream = self.p2psession.new_stream(participant,
Farstream.StreamDirection.BOTH)
self.p2pstream.connect('src-pad-added', on_src_pad_added)
self.p2pstream.set_transmitter_ht('nice', params)
def is_ready(self):
return JingleContent.is_ready(self) and self.candidates_ready
def make_bin_from_config(self, config_key, pipeline, text):
pipeline = pipeline % app.settings.get(config_key)
log.debug('Pipeline: %s', str(pipeline))
try:
gst_bin = Gst.parse_bin_from_description(pipeline, True)
return gst_bin
except GLib.GError as err:
app.nec.push_incoming_event(
InformationEvent(
None,
conn=self.session.connection,
level='error',
pri_txt=_('%s configuration error') % text,
sec_txt=_('Couldnt set up %(text)s. Check your '
'configuration.\nPipeline:\n%(pipeline)s\n'
'Error:\n%(error)s') % {
'text': text,
'pipeline': pipeline,
'error': str(err)}))
raise JingleContentSetupException
def add_remote_candidates(self, candidates):
JingleContent.add_remote_candidates(self, candidates)
# FIXME: connectivity should not be established yet
# Instead, it should be established after session-accept!
if self.sent:
self.p2pstream.add_remote_candidates(candidates)
def batch_dtmf(self, events):
"""
Send several DTMF tones
"""
if self._dtmf_running:
raise Exception("There is a DTMF batch already running")
events = deque(events)
self._dtmf_running = True
self.start_dtmf(events.popleft())
GLib.timeout_add(500, self._next_dtmf, events)
def _next_dtmf(self, events):
self.stop_dtmf()
if events:
self.start_dtmf(events.popleft())
GLib.timeout_add(500, self._next_dtmf, events)
else:
self._dtmf_running = False
def start_dtmf(self, event):
if event in ('*', '#'):
event = {'*': Farstream.DTMFEvent.STAR,
'#': Farstream.DTMFEvent.POUND}[event]
else:
event = int(event)
self.p2psession.start_telephony_event(event, 2)
def stop_dtmf(self):
self.p2psession.stop_telephony_event()
def _fill_content(self, content):
content.addChild(Namespace.JINGLE_RTP + ' description',
attrs={'media': self.media},
payload=list(self.iter_codecs()))
def _setup_funnel(self):
self.funnel = Gst.ElementFactory.make('funnel', None)
self.pipeline.add(self.funnel)
self.funnel.link(self.sink)
self.sink.set_state(Gst.State.PLAYING)
self.funnel.set_state(Gst.State.PLAYING)
def _on_src_pad_added(self, _stream, pad, codec):
log.info('Used codec: %s', codec.to_string())
if not self.funnel:
self._setup_funnel()
pad.link(self.funnel.get_request_pad('sink_%u'))
def _on_gst_message(self, _bus, message):
if message.type == Gst.MessageType.ELEMENT:
name = message.get_structure().get_name()
message_string = message.get_structure().to_string()
log.debug('gst element message: %s', message_string)
if name == 'farstream-new-active-candidate-pair':
pass
elif name == 'farstream-recv-codecs-changed':
pass
elif name == 'farstream-codecs-changed':
if self.sent and self.p2psession.props.codecs_without_config:
self.send_description_info()
if self.transport.remote_candidates:
# those lines MUST be done after we get info on our
# codecs
self.p2pstream.add_remote_candidates(
self.transport.remote_candidates)
self.transport.remote_candidates = []
self.p2pstream.set_property('direction',
Farstream.StreamDirection.BOTH)
elif name == 'farstream-local-candidates-prepared':
self.candidates_ready = True
if self.is_ready():
self.session.on_session_state_changed(self)
elif name == 'farstream-new-local-candidate':
candidate = self.p2pstream.parse_new_local_candidate(message)[1]
self.transport.candidates.append(candidate)
if self.sent:
# FIXME: Is this case even possible?
self.send_candidate(candidate)
elif name == 'farstream-component-state-changed':
state = message.get_structure().get_value('state')
if state == Farstream.StreamState.FAILED:
reason = nbxmpp.Node('reason')
reason.setTag('failed-transport')
self.session.remove_content(self.creator, self.name, reason)
elif name == 'farstream-error':
log.error('Farstream error #%d!\nMessage: %s',
message.get_structure().get_value('error-no'),
message.get_structure().get_value('error-msg'))
elif message.type == Gst.MessageType.ERROR:
# TODO: Fix it to fallback to videotestsrc anytime an error occur,
# or raise an error, Jingle way
# or maybe one-sided stream?
gerror_msg = message.get_structure().get_value('gerror')
debug_msg = message.get_structure().get_value('debug')
log.error(gerror_msg)
log.error(debug_msg)
if not self.stream_failed_once:
app.nec.push_incoming_event(
InformationEvent(
None, dialog_name='gstreamer-error',
kwargs={'error': gerror_msg, 'debug': debug_msg}))
sink_pad = self.p2psession.get_property('sink-pad')
# Remove old source
self.src_bin.get_static_pad('src').unlink(sink_pad)
self.src_bin.set_state(Gst.State.NULL)
self.pipeline.remove(self.src_bin)
if not self.stream_failed_once:
# Add fallback source
self.src_bin = self.get_fallback_src()
self.pipeline.add(self.src_bin)
self.src_bin.get_static_pad('src').link(sink_pad)
self.stream_failed_once = True
else:
reason = nbxmpp.Node('reason')
reason.setTag('failed-application')
self.session.remove_content(self.creator, self.name, reason)
# Start playing again
self.pipeline.set_state(Gst.State.PLAYING)
@staticmethod
def get_fallback_src():
return Gst.ElementFactory.make('fakesrc', None)
def on_negotiated(self):
if self.accepted:
if self.p2psession.get_property('codecs'):
# those lines MUST be done after we get info on our codecs
if self.transport.remote_candidates:
self.p2pstream.add_remote_candidates(
self.transport.remote_candidates)
self.transport.remote_candidates = []
# TODO: Farstream.StreamDirection.BOTH only if senders='both'
# self.p2pstream.set_property('direction',
# Farstream.StreamDirection.BOTH)
JingleContent.on_negotiated(self)
def __on_remote_codecs(self, _stanza, content, _error, _action):
"""
Get peer codecs from what we get from peer
"""
codecs = []
for codec in content.getTag('description').iterTags('payload-type'):
if not codec['id'] or not codec['name'] or not codec['clockrate']:
# ignore invalid payload-types
continue
farstream_codec = Farstream.Codec.new(
int(codec['id']),
codec['name'],
self.farstream_media,
int(codec['clockrate']))
if 'channels' in codec:
farstream_codec.channels = int(codec['channels'])
else:
farstream_codec.channels = 1
for param in codec.iterTags('parameter'):
farstream_codec.add_optional_parameter(
param['name'], str(param['value']))
log.debug('Remote codec: %s (%s)',
codec['name'], codec['clockrate'])
codecs.append(farstream_codec)
if codecs:
try:
self.p2pstream.set_remote_codecs(codecs)
except GLib.Error:
raise FailedApplication
def iter_codecs(self):
codecs = self.p2psession.props.codecs_without_config
for codec in codecs:
attrs = {
'name': codec.encoding_name,
'id': codec.id,
}
if codec.channels > 0:
attrs['channels'] = codec.channels
if codec.clock_rate:
attrs['clockrate'] = codec.clock_rate
if codec.optional_params:
payload = [nbxmpp.Node('parameter',
{'name': p.name, 'value': p.value})
for p in codec.optional_params]
else:
payload = []
yield nbxmpp.Node('payload-type', attrs, payload)
def __stop(self, *things):
self.pipeline.set_state(Gst.State.NULL)
def __del__(self):
self.__stop()
def destroy(self):
JingleContent.destroy(self)
self.p2pstream.disconnect_by_func(self._on_src_pad_added)
self.pipeline.get_bus().disconnect_by_func(self._on_gst_message)
class JingleAudio(JingleRTPContent):
"""
Jingle VoIP sessions consist of audio content transported over an ICE UDP
protocol
"""
def __init__(self, session, transport=None):
JingleRTPContent.__init__(self, session, 'audio', transport)
self.setup_stream()
def set_mic_volume(self, vol):
"""
vol must be between 0 and 1
"""
self.mic_volume.set_property('volume', vol)
def set_out_volume(self, vol):
"""
vol must be between 0 and 1
"""
self.out_volume.set_property('volume', vol)
def setup_stream(self):
JingleRTPContent.setup_stream(self, self._on_src_pad_added)
# list of codecs that are explicitly allowed
allow_codecs = [
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'OPUS',
Farstream.MediaType.AUDIO, 48000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'SPEEX',
Farstream.MediaType.AUDIO, 32000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'G722',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'SPEEX',
Farstream.MediaType.AUDIO, 16000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'PCMA',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'PCMU',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'SPEEX',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'AMR',
Farstream.MediaType.AUDIO, 8000),
]
# disable all other codecs
disable_codecs = []
codecs_without_config = self.p2psession.props.codecs_without_config
allowed_encoding_names = [c.encoding_name for c in allow_codecs]
allowed_encoding_names.append('telephone-event')
for codec in codecs_without_config:
if codec.encoding_name not in allowed_encoding_names:
disable_codecs.append(Farstream.Codec.new(
Farstream.CODEC_ID_DISABLE,
codec.encoding_name,
Farstream.MediaType.AUDIO,
codec.clock_rate))
self.p2psession.set_codec_preferences(allow_codecs + disable_codecs)
# the local parts
# TODO: Add queues?
self.src_bin = self.make_bin_from_config(
'audio_input_device',
'%s ! audioconvert',
_('audio input'))
self.sink = self.make_bin_from_config(
'audio_output_device',
'audioconvert ! volume name=gajim_out_vol ! %s',
_('audio output'))
self.mic_volume = self.src_bin.get_by_name('gajim_vol')
self.out_volume = self.sink.get_by_name('gajim_out_vol')
# link gst elements
self.pipeline.add(self.sink)
self.pipeline.add(self.src_bin)
self.src_bin.get_static_pad('src').link(
self.p2psession.get_property('sink-pad'))
# The following is needed for farstream to process ICE requests:
self.pipeline.set_state(Gst.State.PLAYING)
class JingleVideo(JingleRTPContent):
def __init__(self, session, transport=None):
JingleRTPContent.__init__(self, session, 'video', transport)
self.sink = None
self.setup_stream()
def setup_stream(self):
# TODO: Everything is not working properly:
# sometimes, one window won't show up,
# sometimes it'll freeze...
JingleRTPContent.setup_stream(self, self._on_src_pad_added)
bus = self.pipeline.get_bus()
bus.enable_sync_message_emission()
# list of codecs that are explicitly allowed
# for now only VP8/H264 (available in gst-plugins-good)
allow_codecs = [
#Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'VP9',
# Farstream.MediaType.VIDEO, 90000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'VP8',
Farstream.MediaType.VIDEO, 90000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'H264',
Farstream.MediaType.VIDEO, 90000),
]
# disable all other codecs
disable_codecs = []
codecs_without_config = self.p2psession.props.codecs_without_config
allowed_encoding_names = [c.encoding_name for c in allow_codecs]
for codec in codecs_without_config:
if codec.encoding_name not in allowed_encoding_names:
disable_codecs.append(Farstream.Codec.new(
Farstream.CODEC_ID_DISABLE,
codec.encoding_name,
Farstream.MediaType.VIDEO,
codec.clock_rate))
self.p2psession.set_codec_preferences(allow_codecs + disable_codecs)
def do_setup(self, self_display_sink, other_sink):
if app.settings.get('video_see_self'):
tee = ('! tee name=split ! queue name=self-display-queue split. ! '
'queue name=network-queue')
else:
tee = ''
self.sink = other_sink
self.pipeline.add(self.sink)
self.src_bin = self.make_bin_from_config(
'video_input_device',
'%%s %s' % tee,
_('video input'))
self.pipeline.add(self.src_bin)
if app.settings.get('video_see_self'):
self.pipeline.add(self_display_sink)
self_display_queue = self.src_bin.get_by_name('self-display-queue')
self_display_queue.get_static_pad('src').link_maybe_ghosting(
self_display_sink.get_static_pad('sink'))
self.src_bin.get_static_pad('src').link(
self.p2psession.get_property('sink-pad'))
# The following is needed for farstream to process ICE requests:
self.pipeline.set_state(Gst.State.PLAYING)
if log.getEffectiveLevel() == logging.DEBUG:
# Use 'export GST_DEBUG_DUMP_DOT_DIR=/tmp/' before starting Gajim
timestamp = datetime.now().strftime('%m-%d-%Y-%H-%M-%S')
name = f'video-graph-{timestamp}'
debug_dir = os.environ.get('GST_DEBUG_DUMP_DOT_DIR')
name_dot = f'{debug_dir}/{name}.dot'
name_png = f'{debug_dir}/{name}.png'
Gst.debug_bin_to_dot_file(
self.pipeline, Gst.DebugGraphDetails.ALL, name)
if debug_dir:
try:
os.system(f'dot -Tpng {name_dot} > {name_png}')
except Exception:
log.debug('Could not save pipeline graph. Make sure '
'graphviz is installed.')
def get_fallback_src(self):
# TODO: Use avatar?
pipeline = ('videotestsrc is-live=true ! video/x-raw,framerate=10/1 ! '
'videoconvert')
return Gst.parse_bin_from_description(pipeline, True)
def get_content(desc):
if desc['media'] == 'audio':
return JingleAudio
if desc['media'] == 'video':
return JingleVideo
contents[Namespace.JINGLE_RTP] = get_content

View File

@ -0,0 +1,849 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
"""
Handles Jingle sessions (XEP 0166)
"""
#TODO:
# * 'senders' attribute of 'content' element
# * security preconditions
# * actions:
# - content-modify
# - session-info
# - security-info
# - transport-accept, transport-reject
# - Tie-breaking
# * timeout
import logging
from enum import Enum, unique
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common.jingle_transport import get_jingle_transport
from gajim.common.jingle_transport import JingleTransportIBB
from gajim.common.jingle_content import get_jingle_content
from gajim.common.jingle_content import JingleContentSetupException
from gajim.common.jingle_ft import State
from gajim.common.connection_handlers_events import FilesProp
from gajim.common.nec import NetworkEvent
log = logging.getLogger("app.c.jingle_session")
# FIXME: Move it to JingleSession.States?
@unique
class JingleStates(Enum):
"""
States in which jingle session may exist
"""
ENDED = 0
PENDING = 1
ACTIVE = 2
class OutOfOrder(Exception):
"""
Exception that should be raised when an action is received when in the wrong
state
"""
class TieBreak(Exception):
"""
Exception that should be raised in case of a tie, when we overrule the other
action
"""
class FailedApplication(Exception):
"""
Exception that should be raised in case responder supports none of the
payload-types offered by the initiator
"""
class JingleSession:
"""
This represents one jingle session, that is, one or more content types
negotiated between an initiator and a responder.
"""
def __init__(self, con, weinitiate, jid, iq_id=None, sid=None,
werequest=False):
"""
con -- connection object,
weinitiate -- boolean, are we the initiator?
jid - jid of the other entity
"""
self.contents = {} # negotiated contents
self.connection = con # connection to use
# our full jid
self.ourjid = str(self.connection.get_own_jid())
self.peerjid = jid # jid we connect to
# jid we use as the initiator
self.initiator = self.ourjid if weinitiate else self.peerjid
# jid we use as the responder
self.responder = self.peerjid if weinitiate else self.ourjid
# are we an initiator?
self.weinitiate = weinitiate
# Are we requesting or offering a file?
self.werequest = werequest
self.request = False
# what state is session in? (one from JingleStates)
self.state = JingleStates.ENDED
if not sid:
sid = generate_id()
self.sid = sid # sessionid
# iq stanza id, used to determine which sessions to summon callback
# later on when iq-result stanza arrives
if iq_id is not None:
self.iq_ids = [iq_id]
else:
self.iq_ids = []
self.accepted = True # is this session accepted by user
# Tells whether this session is a file transfer or not
self.session_type_ft = False
# callbacks to call on proper contents
# use .prepend() to add new callbacks, especially when you're going
# to send error instead of ack
self.callbacks = {
'content-accept': [self.__ack, self.__on_content_accept,
self.__broadcast],
'content-add': [self.__ack,
self.__on_content_add, self.__broadcast
], #TODO
'content-modify': [self.__ack], #TODO
'content-reject': [self.__ack, self.__on_content_remove],
'content-remove': [self.__ack, self.__on_content_remove],
'description-info': [self.__ack, self.__broadcast], #TODO
'security-info': [self.__ack], #TODO
'session-accept': [self.__ack, self.__on_session_accept,
self.__on_content_accept,
self.__broadcast],
'session-info': [self.__ack, self.__broadcast,
self.__on_session_info],
'session-initiate': [self.__ack, self.__on_session_initiate,
self.__broadcast],
'session-terminate': [self.__ack, self.__on_session_terminate,
self.__broadcast_all],
'transport-info': [self.__ack, self.__broadcast],
'transport-replace': [self.__ack, self.__broadcast,
self.__on_transport_replace], #TODO
'transport-accept': [self.__ack, self.__on_session_accept,
self.__on_content_accept,
self.__broadcast],
'transport-reject': [self.__ack], #TODO
'iq-result': [self.__broadcast],
'iq-error': [self.__on_error],
}
def collect_iq_id(self, iq_id):
if iq_id is not None:
self.iq_ids.append(iq_id)
def approve_session(self):
"""
Called when user accepts session in UI (when we aren't the initiator)
"""
self.accept_session()
def decline_session(self):
"""
Called when user declines session in UI (when we aren't the initiator)
"""
reason = nbxmpp.Node('reason')
reason.addChild('decline')
self._session_terminate(reason)
def cancel_session(self):
"""
Called when user declines session in UI (when we aren't the initiator)
"""
reason = nbxmpp.Node('reason')
reason.addChild('cancel')
self._session_terminate(reason)
def approve_content(self, media, name=None):
content = self.get_content(media, name)
if content:
content.accepted = True
self.on_session_state_changed(content)
def reject_content(self, media, name=None):
content = self.get_content(media, name)
if content:
if self.state == JingleStates.ACTIVE:
self.__content_reject(content)
content.destroy()
self.on_session_state_changed()
def end_session(self):
"""
Called when user stops or cancel session in UI
"""
reason = nbxmpp.Node('reason')
if self.state == JingleStates.ACTIVE:
reason.addChild('success')
else:
reason.addChild('cancel')
self._session_terminate(reason)
def get_content(self, media=None, name=None):
if media is None:
return
for content in self.contents.values():
if content.media == media:
if name is None or content.name == name:
return content
def add_content(self, name, content, creator='we'):
"""
Add new content to session. If the session is active, this will send
proper stanza to update session
Creator must be one of ('we', 'peer', 'initiator', 'responder')
"""
assert creator in ('we', 'peer', 'initiator', 'responder')
if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \
not self.weinitiate):
creator = 'initiator'
elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \
not self.weinitiate):
creator = 'responder'
content.creator = creator
content.name = name
self.contents[(creator, name)] = content
if (creator == 'initiator') == self.weinitiate:
# The content is from us, accept it
content.accepted = True
def remove_content(self, creator, name, reason=None):
"""
Remove the content `name` created by `creator`
by sending content-remove, or by sending session-terminate if
there is no content left.
"""
if (creator, name) in self.contents:
content = self.contents[(creator, name)]
self.__content_remove(content, reason)
self.contents[(creator, name)].destroy()
if not self.contents:
self.end_session()
def modify_content(self, creator, name, transport=None):
'''
Currently used for transport replacement
'''
content = self.contents[(creator, name)]
file_props = content.transport.file_props
file_props.transport_sid = transport.sid
transport.set_file_props(file_props)
content.transport = transport
# The content will have to be resend now that it is modified
content.sent = False
content.accepted = True
def on_session_state_changed(self, content=None):
if self.state == JingleStates.ENDED:
# Session not yet started, only one action possible: session-initiate
if self.is_ready() and self.weinitiate:
self.__session_initiate()
elif self.state == JingleStates.PENDING:
# We can either send a session-accept or a content-add
if self.is_ready() and not self.weinitiate:
self.__session_accept()
elif content and (content.creator == 'initiator') == self.weinitiate:
self.__content_add(content)
elif content and self.weinitiate:
self.__content_accept(content)
elif self.state == JingleStates.ACTIVE:
# We can either send a content-add or a content-accept. However, if
# we are sending a file we can only use session_initiate.
if not content:
return
we_created_content = (content.creator == 'initiator') \
== self.weinitiate
if we_created_content and content.media == 'file':
self.__session_initiate()
if we_created_content:
# We initiated this content. It's a pending content-add.
self.__content_add(content)
else:
# The other side created this content, we accept it.
self.__content_accept(content)
def is_ready(self):
"""
Return True when all codecs and candidates are ready (for all contents)
"""
return (all((content.is_ready() for content in self.contents.values()))
and self.accepted)
def accept_session(self):
"""
Mark the session as accepted
"""
self.accepted = True
self.on_session_state_changed()
def start_session(self):
"""
Mark the session as ready to be started
"""
self.accepted = True
self.on_session_state_changed()
def send_session_info(self):
pass
def send_content_accept(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-accept')
jingle.addChild(node=content)
self.connection.connection.send(stanza)
def send_transport_info(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('transport-info')
jingle.addChild(node=content)
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
def send_description_info(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('description-info')
jingle.addChild(node=content)
self.connection.connection.send(stanza)
def on_stanza(self, stanza):
"""
A callback for ConnectionJingle. It gets stanza, then tries to send it to
all internally registered callbacks. First one to raise
nbxmpp.NodeProcessed breaks function
"""
jingle = stanza.getTag('jingle')
error = stanza.getTag('error')
if error:
# it's an iq-error stanza
action = 'iq-error'
elif jingle:
# it's a jingle action
action = jingle.getAttr('action')
if action not in self.callbacks:
self.__send_error(stanza, 'bad-request')
return
# FIXME: If we aren't initiated and it's not a session-initiate...
if action not in ['session-initiate', 'session-terminate'] \
and self.state == JingleStates.ENDED:
self.__send_error(stanza, 'item-not-found', 'unknown-session')
return
else:
# it's an iq-result (ack) stanza
action = 'iq-result'
callables = self.callbacks[action]
try:
for call in callables:
call(stanza=stanza, jingle=jingle, error=error, action=action)
except nbxmpp.NodeProcessed:
pass
except TieBreak:
self.__send_error(stanza, 'conflict', 'tiebreak')
except OutOfOrder:
# FIXME
self.__send_error(stanza, 'unexpected-request', 'out-of-order')
except FailedApplication:
reason = nbxmpp.Node('reason')
reason.addChild('failed-application')
self._session_terminate(reason)
def __ack(self, stanza, jingle, error, action):
"""
Default callback for action stanzas -- simple ack and stop processing
"""
response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.connection.connection.send(response)
def __on_error(self, stanza, jingle, error, action):
# FIXME
text = error.getTagData('text')
error_name = None
for child in error.getChildren():
if child.getNamespace() == Namespace.JINGLE_ERRORS:
error_name = child.getName()
break
if child.getNamespace() == Namespace.STANZAS:
error_name = child.getName()
self.__dispatch_error(error_name, text, error.getAttr('type'))
def transport_replace(self):
transport = JingleTransportIBB()
# For debug only, delete this and replace for a function
# that will identify contents by its sid
for creator, name in self.contents:
self.modify_content(creator, name, transport)
cont = self.contents[(creator, name)]
cont.transport = transport
stanza, jingle = self.__make_jingle('transport-replace')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'transport-replace')
self.connection.connection.send(stanza)
self.state = JingleStates.PENDING
def __on_transport_replace(self, stanza, jingle, error, action):
for content in jingle.iterTags('content'):
creator = content['creator']
name = content['name']
if (creator, name) in self.contents:
transport_ns = content.getTag('transport').getNamespace()
if transport_ns == Namespace.JINGLE_ICE_UDP:
# FIXME: We don't manage anything else than ICE-UDP now...
# What was the previous transport?!?
# Anyway, content's transport is not modifiable yet
pass
elif transport_ns == Namespace.JINGLE_IBB:
transport = JingleTransportIBB(node=content.getTag(
'transport'))
self.modify_content(creator, name, transport)
self.state = JingleStates.PENDING
self.contents[(creator, name)].state = State.TRANSPORT_REPLACE
self.__ack(stanza, jingle, error, action)
self.__transport_accept(transport)
else:
stanza, jingle = self.__make_jingle('transport-reject')
content = jingle.setTag('content', attrs={'creator': creator,
'name': name})
content.setTag('transport', namespace=transport_ns)
self.connection.connection.send(stanza)
raise nbxmpp.NodeProcessed
else:
# FIXME: This resource is unknown to us, what should we do?
# For now, reject the transport
stanza, jingle = self.__make_jingle('transport-reject')
content = jingle.setTag('content', attrs={'creator': creator,
'name': name})
content.setTag('transport', namespace=transport_ns)
self.connection.connection.send(stanza)
raise nbxmpp.NodeProcessed
def __on_session_info(self, stanza, jingle, error, action):
# TODO: active, (un)hold, (un)mute
payload = jingle.getPayload()
if payload[0].getName() == 'ringing':
# ignore ringing
raise nbxmpp.NodeProcessed
if self.state != JingleStates.ACTIVE:
raise OutOfOrder
for child in payload:
if child.getName() == 'checksum':
hash_ = child.getTag('file').getTag(name='hash',
namespace=Namespace.HASHES_2)
if hash_ is None:
continue
algo = hash_.getAttr('algo')
if algo in nbxmpp.Hashes2.supported:
file_props = FilesProp.getFileProp(self.connection.name,
self.sid)
file_props.algo = algo
file_props.hash_ = hash_.getData()
raise nbxmpp.NodeProcessed
self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info',
type_='modify')
raise nbxmpp.NodeProcessed
def __on_content_remove(self, stanza, jingle, error, action):
for content in jingle.iterTags('content'):
creator = content['creator']
name = content['name']
if (creator, name) in self.contents:
content = self.contents[(creator, name)]
# TODO: this will fail if content is not an RTP content
self._raise_event('jingle-disconnected-received',
media=content.media,
reason='removed')
content.destroy()
if not self.contents:
reason = nbxmpp.Node('reason')
reason.setTag('success')
self._session_terminate(reason)
def __on_session_accept(self, stanza, jingle, error, action):
# FIXME
if self.state != JingleStates.PENDING:
raise OutOfOrder
self.state = JingleStates.ACTIVE
@staticmethod
def __on_content_accept(stanza, jingle, error, action):
"""
Called when we get content-accept stanza or equivalent one (like
session-accept)
"""
# check which contents are accepted
# for content in jingle.iterTags('content'):
# creator = content['creator']
# name = content['name']
return
def __on_content_add(self, stanza, jingle, error, action):
if self.state == JingleStates.ENDED:
raise OutOfOrder
parse_result = self.__parse_contents(jingle)
contents = parse_result[0]
# rejected_contents = parse_result[1]
# for name, creator in rejected_contents:
# content = JingleContent()
# self.add_content(name, content, creator)
# self.__content_reject(content)
# self.contents[(content.creator, content.name)].destroy()
self._raise_event('jingle-request-received', contents=contents)
def __on_session_initiate(self, stanza, jingle, error, action):
"""
We got a jingle session request from other entity, therefore we are the
receiver... Unpack the data, inform the user
"""
if self.state != JingleStates.ENDED:
raise OutOfOrder
self.initiator = jingle['initiator']
self.responder = self.ourjid
self.peerjid = self.initiator
self.accepted = False # user did not accept this session yet
# TODO: If the initiator is unknown to the receiver (e.g., via presence
# subscription) and the receiver has a policy of not communicating via
# Jingle with unknown entities, it SHOULD return a <service-unavailable/>
# error.
# Lets check what kind of jingle session does the peer want
contents, _contents_rejected, reason_txt = self.__parse_contents(jingle)
# If there's no content we understand...
if not contents:
# TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate
reason = nbxmpp.Node('reason')
reason.setTag(reason_txt)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise nbxmpp.NodeProcessed
# If we are not receiving a file
# Check if there's already a session with this user:
if contents[0].media != 'file':
for session in self.connection.get_module('Jingle').get_jingle_sessions(self.peerjid):
if session is not self:
reason = nbxmpp.Node('reason')
alternative_session = reason.setTag('alternative-session')
alternative_session.setTagData('sid', session.sid)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise nbxmpp.NodeProcessed
else:
# Stop if we don't have the requested file or the peer is not
# allowed to request the file
request = contents[0].senders == 'responder'
if request:
self.request = True
hash_tag = request.getTag('file').getTag('hash')
hash_data = hash_tag.getData() if hash_tag else None
n = request.getTag('file').getTag('name')
n = n.getData() if n else None
pjid = app.get_jid_without_resource(self.peerjid)
file_info = self.connection.get_module('Jingle').get_file_info(
pjid, hash_data, n, self.connection.name)
if not file_info:
log.warning('The peer %s is requesting a ' \
'file that we dont have or ' \
'it is not allowed to request', pjid)
self.decline_session()
raise nbxmpp.NodeProcessed
self.state = JingleStates.PENDING
# Send event about starting a session
self._raise_event('jingle-request-received', contents=contents)
def __broadcast(self, stanza, jingle, error, action):
"""
Broadcast the stanza contents to proper content handlers
"""
#if jingle is None: # it is a iq-result stanza
# for cn in self.contents.values():
# cn.on_stanza(stanza, None, error, action)
# return
# special case: iq-result stanza does not come with a jingle element
if action == 'iq-result':
for cn in self.contents.values():
cn.on_stanza(stanza, None, error, action)
return
for content in jingle.iterTags('content'):
name = content['name']
creator = content['creator']
if (creator, name) not in self.contents:
text = 'Content %s (created by %s) does not exist' % (name, creator)
self.__send_error(stanza, 'bad-request', text=text, type_='modify')
raise nbxmpp.NodeProcessed
cn = self.contents[(creator, name)]
cn.on_stanza(stanza, content, error, action)
def __on_session_terminate(self, stanza, jingle, error, action):
self.connection.get_module('Jingle').delete_jingle_session(self.sid)
reason, text = self.__reason_from_stanza(jingle)
if reason not in ('success', 'cancel', 'decline'):
self.__dispatch_error(reason, text)
if text:
text = '%s (%s)' % (reason, text)
else:
# TODO
text = reason
if reason == 'decline':
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
if reason == 'success':
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
if reason == 'cancel' and self.session_type_ft:
self._raise_event('jingle-ft-cancelled-received',
media=None,
reason=text)
def __broadcast_all(self, stanza, jingle, error, action):
"""
Broadcast the stanza to all content handlers
"""
for content in self.contents.values():
content.on_stanza(stanza, None, error, action)
def __parse_contents(self, jingle):
# TODO: Needs some reworking
contents = []
contents_rejected = []
reasons = set()
for element in jingle.iterTags('content'):
transport = get_jingle_transport(element.getTag('transport'))
if transport:
transport.ourjid = self.ourjid
content_type = get_jingle_content(element.getTag('description'))
if content_type:
try:
if transport:
content = content_type(self, transport=transport)
self.add_content(element['name'],
content, 'peer')
contents.append(content)
else:
reasons.add('unsupported-transports')
contents_rejected.append((element['name'], 'peer'))
except JingleContentSetupException:
reasons.add('failed-application')
else:
contents_rejected.append((element['name'], 'peer'))
reasons.add('unsupported-applications')
failure_reason = None
# Store the first reason of failure
for reason in ('failed-application', 'unsupported-transports',
'unsupported-applications'):
if reason in reasons:
failure_reason = reason
break
return (contents, contents_rejected, failure_reason)
def __dispatch_error(self, error=None, text=None, type_=None):
if text:
text = '%s (%s)' % (error, text)
if type_ != 'modify':
self._raise_event('jingle-error-received', reason=text or error)
@staticmethod
def __reason_from_stanza(stanza):
# TODO: Move to GUI?
reason = 'success'
reasons = [
'success', 'busy', 'cancel', 'connectivity-error', 'decline',
'expired', 'failed-application', 'failed-transport',
'general-error', 'gone', 'incompatible-parameters', 'media-error',
'security-error', 'timeout', 'unsupported-applications',
'unsupported-transports'
]
tag = stanza.getTag('reason')
text = ''
if tag:
text = tag.getTagData('text')
for r in reasons:
if tag.getTag(r):
reason = r
break
return (reason, text)
def __make_jingle(self, action, reason=None):
stanza = nbxmpp.Iq(typ='set', to=nbxmpp.JID.from_string(self.peerjid),
frm=self.ourjid)
attrs = {
'action': action,
'sid': self.sid,
'initiator' : self.initiator
}
jingle = stanza.addChild('jingle', attrs=attrs,
namespace=Namespace.JINGLE)
if reason is not None:
jingle.addChild(node=reason)
return stanza, jingle
def __send_error(self, stanza, error, jingle_error=None, text=None, type_=None):
err_stanza = nbxmpp.Error(stanza, '%s %s' % (Namespace.STANZAS, error))
err = err_stanza.getTag('error')
if type_:
err.setAttr('type', type_)
if jingle_error:
err.setTag(jingle_error, namespace=Namespace.JINGLE_ERRORS)
if text:
err.setTagData('text', text)
self.connection.connection.send(err_stanza)
self.__dispatch_error(jingle_error or error, text, type_)
@staticmethod
def __append_content(jingle, content):
"""
Append <content/> element to <jingle/> element
"""
jingle.addChild('content',
attrs={'name': content.name,
'creator': content.creator,
'senders': content.senders})
def __append_contents(self, jingle):
"""
Append all <content/> elements to <jingle/>
"""
# TODO: integrate with __appendContent?
# TODO: parameters 'name', 'content'?
for content in self.contents.values():
if content.is_ready():
self.__append_content(jingle, content)
def __session_initiate(self):
assert self.state == JingleStates.ENDED
stanza, jingle = self.__make_jingle('session-initiate')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-initiate-sent')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.PENDING
def __session_accept(self):
assert self.state == JingleStates.PENDING
stanza, jingle = self.__make_jingle('session-accept')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-accept-sent')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.ACTIVE
def __session_info(self, payload=None):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('session-info')
if payload:
jingle.addChild(node=payload)
self.connection.connection.send(stanza)
def _JingleFileTransfer__session_info(self, payload):
# For some strange reason when I call
# self.session.__session_info(payload) from the jingleFileTransfer object
# within a thread, this method gets called instead. Even though, it
# isn't being called explicitly.
self.__session_info(payload)
def _session_terminate(self, reason=None):
stanza, jingle = self.__make_jingle('session-terminate', reason=reason)
self.__broadcast_all(stanza, jingle, None, 'session-terminate-sent')
if self.connection.connection and self.connection.state.is_available:
self.connection.connection.send(stanza)
# TODO: Move to GUI?
reason, text = self.__reason_from_stanza(jingle)
if reason not in ('success', 'cancel', 'decline'):
self.__dispatch_error(reason, text)
if text:
text = '%s (%s)' % (reason, text)
else:
text = reason
self.connection.get_module('Jingle').delete_jingle_session(self.sid)
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
def __content_add(self, content):
# TODO: test
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-add')
self.__append_content(jingle, content)
self.__broadcast(stanza, jingle, None, 'content-add-sent')
id_ = self.connection.connection.send(stanza)
self.collect_iq_id(id_)
def __content_accept(self, content):
# TODO: test
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-accept')
self.__append_content(jingle, content)
self.__broadcast(stanza, jingle, None, 'content-accept-sent')
id_ = self.connection.connection.send(stanza)
self.collect_iq_id(id_)
def __content_reject(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-reject')
self.__append_content(jingle, content)
self.connection.connection.send(stanza)
# TODO: this will fail if content is not an RTP content
self._raise_event('jingle-disconnected-received',
media=content.media,
reason='rejected')
def __content_modify(self):
assert self.state != JingleStates.ENDED
def __content_remove(self, content, reason=None):
assert self.state != JingleStates.ENDED
if self.connection.connection and self.connection.state.is_available:
stanza, jingle = self.__make_jingle('content-remove', reason=reason)
self.__append_content(jingle, content)
self.connection.connection.send(stanza)
# TODO: this will fail if content is not an RTP content
self._raise_event('jingle-disconnected-received',
media=content.media,
reason='removed')
def content_negotiated(self, media):
self._raise_event('jingle-connected-received', media=media)
def __transport_accept(self, transport):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('transport-accept')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'transport-accept')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.ACTIVE
def _raise_event(self, name, **kwargs):
jid, resource = app.get_room_and_nick_from_fjid(self.peerjid)
app.nec.push_incoming_event(
NetworkEvent(name,
conn=self.connection,
fjid=self.peerjid,
jid=jid,
sid=self.sid,
resource=resource,
jingle_session=self,
**kwargs))

View File

@ -0,0 +1,502 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
"""
Handles Jingle Transports (currently only ICE-UDP)
"""
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
import logging
import socket
from enum import IntEnum, unique
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from gajim.common import app
log = logging.getLogger('gajim.c.jingle_transport')
transports = {} # type: Dict[str, Any]
def get_jingle_transport(node):
namespace = node.getNamespace()
if namespace in transports:
return transports[namespace](node)
@unique
class TransportType(IntEnum):
"""
Possible types of a JingleTransport
"""
ICEUDP = 1
SOCKS5 = 2
IBB = 3
class JingleTransport:
"""
An abstraction of a transport in Jingle sessions
"""
__slots__ = ['type_', 'candidates', 'remote_candidates', 'connection',
'file_props', 'ourjid', 'sid']
def __init__(self, type_):
self.type_ = type_
self.candidates = []
self.remote_candidates = []
self.connection = None
self.file_props = None
self.ourjid = None
self.sid = None
def _iter_candidates(self):
for candidate in self.candidates:
yield self.make_candidate(candidate)
def make_candidate(self, candidate):
"""
Build a candidate stanza for the given candidate
"""
def make_transport(self, candidates=None):
"""
Build a transport stanza with the given candidates (or self.candidates if
candidates is None)
"""
if not candidates:
candidates = list(self._iter_candidates())
else:
candidates = (self.make_candidate(candidate) for candidate in candidates)
transport = nbxmpp.Node('transport', payload=candidates)
return transport
def parse_transport_stanza(self, transport):
"""
Return the list of transport candidates from a transport stanza
"""
return []
def set_connection(self, conn):
self.connection = conn
if not self.sid:
self.sid = generate_id()
def set_file_props(self, file_props):
self.file_props = file_props
def set_our_jid(self, jid):
self.ourjid = jid
def set_sid(self, sid):
self.sid = sid
class JingleTransportSocks5(JingleTransport):
"""
Socks5 transport in jingle scenario
Note: Don't forget to call set_file_props after initialization
"""
def __init__(self, node=None):
JingleTransport.__init__(self, TransportType.SOCKS5)
self.connection = None
self.remote_candidates = []
self.sid = None
if node and node.getAttr('sid'):
self.sid = node.getAttr('sid')
def make_candidate(self, candidate):
log.info('candidate dict, %s', candidate)
attrs = {
'cid': candidate['candidate_id'],
'host': candidate['host'],
'jid': candidate['jid'],
'port': candidate['port'],
'priority': candidate['priority'],
'type': candidate['type']
}
return nbxmpp.Node('candidate', attrs=attrs)
def make_transport(self, candidates=None, add_candidates=True):
if add_candidates:
self._add_local_ips_as_candidates()
self._add_additional_candidates()
self._add_proxy_candidates()
transport = JingleTransport.make_transport(self, candidates)
else:
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
transport.setAttr('sid', self.sid)
if self.file_props.dstaddr:
transport.setAttr('dstaddr', self.file_props.dstaddr)
return transport
def parse_transport_stanza(self, transport):
candidates = []
for candidate in transport.iterTags('candidate'):
typ = 'direct' # default value
if candidate.has_attr('type'):
typ = candidate['type']
cand = {
'state': 0,
'target': self.ourjid,
'host': candidate['host'],
'port': int(candidate['port']),
'candidate_id': candidate['cid'],
'type': typ,
'priority': candidate['priority']
}
candidates.append(cand)
# we need this when we construct file_props on session-initiation
if candidates:
self.remote_candidates = candidates
return candidates
def _add_candidates(self, candidates):
for cand in candidates:
in_remote = False
for cand2 in self.remote_candidates:
if cand['host'] == cand2['host'] and \
cand['port'] == cand2['port']:
in_remote = True
break
if not in_remote:
self.candidates.append(cand)
def _add_local_ips_as_candidates(self):
if not app.settings.get_account_setting(self.connection.name,
'ft_send_local_ips'):
return
if not self.connection:
return
port = int(app.settings.get('file_transfers_port'))
#type preference of connection type. XEP-0260 section 2.2
type_preference = 126
priority = (2**16) * type_preference
hosts = set()
local_ip_cand = []
my_ip = self.connection.local_address
if my_ip is None:
log.warning('No local address available')
else:
candidate = {
'host': my_ip,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority
}
hosts.add(my_ip)
local_ip_cand.append(candidate)
try:
for addrinfo in socket.getaddrinfo(socket.gethostname(), None):
addr = addrinfo[4][0]
if not addr in hosts and not addr.startswith('127.') and \
addr != '::1':
candidate = {
'host': addr,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
hosts.add(addr)
local_ip_cand.append(candidate)
except socket.gaierror:
pass # ignore address-related errors for getaddrinfo
try:
from netifaces import interfaces, ifaddresses, AF_INET, AF_INET6
for ifaceName in interfaces():
addresses = ifaddresses(ifaceName)
if AF_INET in addresses:
for address in addresses[AF_INET]:
addr = address['addr']
if addr in hosts or addr.startswith('127.'):
continue
candidate = {
'host': addr,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
hosts.add(addr)
local_ip_cand.append(candidate)
if AF_INET6 in addresses:
for address in addresses[AF_INET6]:
addr = address['addr']
if addr in hosts or addr.startswith('::1') or \
addr.count(':') != 7:
continue
candidate = {
'host': addr,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
hosts.add(addr)
local_ip_cand.append(candidate)
except ImportError:
pass
self._add_candidates(local_ip_cand)
def _add_additional_candidates(self):
if not self.connection:
return
type_preference = 126
priority = (2**16) * type_preference
additional_ip_cand = []
port = int(app.settings.get('file_transfers_port'))
ft_add_hosts = app.settings.get('ft_add_hosts_to_send')
if ft_add_hosts:
hosts = [e.strip() for e in ft_add_hosts.split(',')]
for host in hosts:
candidate = {
'host': host,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
additional_ip_cand.append(candidate)
self._add_candidates(additional_ip_cand)
def _add_proxy_candidates(self):
if not self.connection:
return
type_preference = 10
priority = (2**16) * type_preference
proxy_cand = []
socks5conn = self.connection
proxyhosts = socks5conn.get_module('Bytestream')._get_file_transfer_proxies_from_config(self.file_props)
if proxyhosts:
self.file_props.proxyhosts = proxyhosts
for proxyhost in proxyhosts:
candidate = {
'host': proxyhost['host'],
'candidate_id': generate_id(),
'port': int(proxyhost['port']),
'type': 'proxy',
'jid': proxyhost['jid'],
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
proxy_cand.append(candidate)
self._add_candidates(proxy_cand)
def get_content(self):
sesn = self.connection.get_module('Jingle').get_jingle_session(
self.ourjid, self.file_props.sid)
for content in sesn.contents.values():
if content.transport == self:
return content
def _on_proxy_auth_ok(self, proxy):
log.info('proxy auth ok for %s', str(proxy))
# send activate request to proxy, send activated confirmation to peer
if not self.connection:
return
sesn = self.connection.get_module('Jingle').get_jingle_session(
self.ourjid, self.file_props.sid)
if sesn is None:
return
iq = nbxmpp.Iq(to=proxy['jid'], frm=self.ourjid, typ='set')
auth_id = "au_" + proxy['sid']
iq.setID(auth_id)
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
query.setAttr('sid', proxy['sid'])
activate = query.setTag('activate')
activate.setData(sesn.peerjid)
iq.setID(auth_id)
self.connection.connection.send(iq)
content = nbxmpp.Node('content')
content.setAttr('creator', 'initiator')
content_object = self.get_content()
content.setAttr('name', content_object.name)
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
transport.setAttr('sid', proxy['sid'])
activated = nbxmpp.Node('activated')
cid = None
if 'cid' in proxy:
cid = proxy['cid']
else:
for host in self.candidates:
if host['host'] == proxy['host'] and host['jid'] == proxy['jid'] \
and host['port'] == proxy['port']:
cid = host['candidate_id']
break
if cid is None:
raise Exception('cid is missing')
activated.setAttr('cid', cid)
transport.addChild(node=activated)
content.addChild(node=transport)
sesn.send_transport_info(content)
class JingleTransportIBB(JingleTransport):
def __init__(self, node=None, block_sz=None):
JingleTransport.__init__(self, TransportType.IBB)
if block_sz:
self.block_sz = block_sz
else:
self.block_sz = '4096'
self.connection = None
self.sid = None
if node and node.getAttr('sid'):
self.sid = node.getAttr('sid')
def make_transport(self):
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_IBB)
transport.setAttr('block-size', self.block_sz)
transport.setAttr('sid', self.sid)
return transport
try:
from gi.repository import Farstream
except ImportError:
pass
class JingleTransportICEUDP(JingleTransport):
def __init__(self, node):
JingleTransport.__init__(self, TransportType.ICEUDP)
def make_candidate(self, candidate):
types = {
Farstream.CandidateType.HOST: 'host',
Farstream.CandidateType.SRFLX: 'srflx',
Farstream.CandidateType.PRFLX: 'prflx',
Farstream.CandidateType.RELAY: 'relay',
Farstream.CandidateType.MULTICAST: 'multicast'
}
attrs = {
'component': candidate.component_id,
'foundation': '1', # hack
'generation': '0',
'ip': candidate.ip,
'network': '0',
'port': candidate.port,
'priority': int(candidate.priority), # hack
'id': app.get_an_id()
}
if candidate.type in types:
attrs['type'] = types[candidate.type]
if candidate.proto == Farstream.NetworkProtocol.UDP:
attrs['protocol'] = 'udp'
else:
# we actually don't handle properly different tcp options in jingle
attrs['protocol'] = 'tcp'
return nbxmpp.Node('candidate', attrs=attrs)
def make_transport(self, candidates=None):
transport = JingleTransport.make_transport(self, candidates)
transport.setNamespace(Namespace.JINGLE_ICE_UDP)
if self.candidates and self.candidates[0].username and \
self.candidates[0].password:
transport.setAttr('ufrag', self.candidates[0].username)
transport.setAttr('pwd', self.candidates[0].password)
return transport
def parse_transport_stanza(self, transport):
candidates = []
for candidate in transport.iterTags('candidate'):
foundation = str(candidate['foundation'])
component_id = int(candidate['component'])
ip = str(candidate['ip'])
port = int(candidate['port'])
base_ip = None
base_port = 0
if candidate['protocol'] == 'udp':
proto = Farstream.NetworkProtocol.UDP
else:
# we actually don't handle properly different tcp options in
# jingle
proto = Farstream.NetworkProtocol.TCP
priority = int(candidate['priority'])
types = {
'host': Farstream.CandidateType.HOST,
'srflx': Farstream.CandidateType.SRFLX,
'prflx': Farstream.CandidateType.PRFLX,
'relay': Farstream.CandidateType.RELAY,
'multicast': Farstream.CandidateType.MULTICAST
}
if 'type' in candidate and candidate['type'] in types:
type_ = types[candidate['type']]
else:
log.warning('Unknown type %s', candidate['type'])
type_ = Farstream.CandidateType.HOST
username = str(transport['ufrag'])
password = str(transport['pwd'])
ttl = 0
cand = Farstream.Candidate.new_full(foundation, component_id, ip,
port, base_ip, base_port,
proto, priority, type_,
username, password, ttl)
candidates.append(cand)
self.remote_candidates.extend(candidates)
return candidates
transports[Namespace.JINGLE_ICE_UDP] = JingleTransportICEUDP
transports[Namespace.JINGLE_BYTESTREAM] = JingleTransportSocks5
transports[Namespace.JINGLE_IBB] = JingleTransportIBB

282
gajim/common/jingle_xtls.py Normal file
View File

@ -0,0 +1,282 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
from pathlib import Path
from OpenSSL import SSL, crypto
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common import configpaths
log = logging.getLogger('gajim.c.jingle_xtls')
# key-exchange id -> [callback, args], accept that session once key-exchange completes
pending_contents = {}
def key_exchange_pend(id_, cb, args):
# args is a list
pending_contents[id_] = [cb, args]
def approve_pending_content(id_):
cb = pending_contents[id_][0]
args = pending_contents[id_][1]
cb(*args)
TYPE_RSA = crypto.TYPE_RSA
TYPE_DSA = crypto.TYPE_DSA
SELF_SIGNED_CERTIFICATE = 'localcert'
DH_PARAMS = 'dh_params.pem'
DEFAULT_DH_PARAMS = 'dh4096.pem'
def default_callback(connection, certificate, error_num, depth, return_code):
log.info("certificate: %s", certificate)
return return_code
def load_cert_file(cert_path, cert_store=None):
"""
This is almost identical to the one in nbxmpp.tls_nb
"""
if not cert_path.is_file():
return None
try:
f = open(cert_path)
except IOError as e:
log.warning('Unable to open certificate file %s: %s', cert_path,
str(e))
return None
lines = f.readlines()
i = 0
begin = -1
for line in lines:
if 'BEGIN CERTIFICATE' in line:
begin = i
elif 'END CERTIFICATE' in line and begin > -1:
cert = ''.join(lines[begin:i+2])
try:
x509cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
if cert_store:
cert_store.add_cert(x509cert)
f.close()
return x509cert
except crypto.Error as exception_obj:
log.warning('Unable to load a certificate from file %s: %s',
cert_path, exception_obj.args[0][0][2])
except Exception:
log.warning('Unknown error while loading certificate from file '
'%s', cert_path)
begin = -1
i += 1
f.close()
def get_context(fingerprint, verify_cb=None, remote_jid=None):
"""
constructs and returns the context objects
"""
ctx = SSL.Context(SSL.SSLv23_METHOD)
flags = (SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 | SSL.OP_SINGLE_DH_USE \
| SSL.OP_NO_TICKET)
ctx.set_options(flags)
ctx.set_cipher_list(b'HIGH:!aNULL:!3DES')
if fingerprint == 'server': # for testing purposes only
ctx.set_verify(SSL.VERIFY_NONE|SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
verify_cb or default_callback)
elif fingerprint == 'client':
ctx.set_verify(SSL.VERIFY_PEER, verify_cb or default_callback)
cert_name = configpaths.get('MY_CERT') / SELF_SIGNED_CERTIFICATE
ctx.use_privatekey_file(str(cert_name.with_suffix('.pkey')).encode('utf-8'))
ctx.use_certificate_file(str(cert_name.with_suffix('.cert')).encode('utf-8'))
# Try to load Diffie-Hellman parameters.
# First try user DH parameters, if this fails load the default DH parameters
dh_params_name = configpaths.get('MY_CERT') / DH_PARAMS
try:
with open(dh_params_name, "r"):
ctx.load_tmp_dh(dh_params_name.encode('utf-8'))
except FileNotFoundError as err:
default_dh_params_name = (configpaths.get('DATA') / 'other' /
DEFAULT_DH_PARAMS)
try:
with open(default_dh_params_name, "r"):
ctx.load_tmp_dh(str(default_dh_params_name).encode('utf-8'))
except FileNotFoundError as err:
log.error('Unable to load default DH parameter file: %s, %s',
default_dh_params_name, err)
raise
if remote_jid:
store = ctx.get_cert_store()
path = configpaths.get('MY_PEER_CERTS').expanduser() / (remote_jid
+ '.cert')
if path.exists():
load_cert_file(path, cert_store=store)
log.debug('certificate file %s loaded fingerprint %s',
path, fingerprint)
return ctx
def read_cert(certpath):
certificate = ''
with open(certpath, 'r') as certfile:
for line in certfile.readlines():
if not line.startswith('-'):
certificate += line
return certificate
def send_cert(con, jid_from, sid):
certpath = configpaths.get('MY_CERT') / (SELF_SIGNED_CERTIFICATE
+ '.cert')
certificate = read_cert(certpath)
iq = nbxmpp.Iq('result', to=jid_from)
iq.setAttr('id', sid)
pubkey = iq.setTag('pubkeys')
pubkey.setNamespace(Namespace.PUBKEY_PUBKEY)
keyinfo = pubkey.setTag('keyinfo')
name = keyinfo.setTag('name')
name.setData('CertificateHash')
cert = keyinfo.setTag('x509cert')
cert.setData(certificate)
con.send(iq)
def handle_new_cert(con, obj, jid_from):
jid = app.get_jid_without_resource(jid_from)
certpath = configpaths.get('MY_PEER_CERTS').expanduser() / (jid + '.cert')
id_ = obj.getAttr('id')
x509cert = obj.getTag('pubkeys').getTag('keyinfo').getTag('x509cert')
cert = x509cert.getData()
f = open(certpath, 'w')
f.write('-----BEGIN CERTIFICATE-----\n')
f.write(cert)
f.write('-----END CERTIFICATE-----\n')
f.close()
approve_pending_content(id_)
def check_cert(jid, fingerprint):
certpath = configpaths.get('MY_PEER_CERTS').expanduser() / (jid + '.cert')
if certpath.exists():
cert = load_cert_file(certpath)
if cert:
digest_algo = cert.get_signature_algorithm().decode('utf-8')\
.split('With')[0]
if cert.digest(digest_algo) == fingerprint:
return True
return False
def send_cert_request(con, to_jid):
iq = nbxmpp.Iq('get', to=to_jid)
id_ = generate_id()
iq.setAttr('id', id_)
pubkey = iq.setTag('pubkeys')
pubkey.setNamespace(Namespace.PUBKEY_PUBKEY)
con.connection.send(iq)
return str(id_)
# the following code is partly due to pyopenssl examples
def createKeyPair(type_, bits):
"""
Create a public/private key pair.
Arguments: type_ - Key type, must be one of TYPE_RSA and TYPE_DSA
bits - Number of bits to use in the key
Returns: The public/private key pair in a PKey object
"""
pkey = crypto.PKey()
pkey.generate_key(type_, bits)
return pkey
def createCertRequest(pkey, digest="sha256", **name):
"""
Create a certificate request.
Arguments: pkey - The key to associate with the request
digest - Digestion method to use for signing, default is sha256
**name - The name of the subject of the request, possible
arguments are:
C - Country name
ST - State or province name
L - Locality name
O - Organization name
OU - Organizational unit name
CN - Common name
emailAddress - E-mail address
Returns: The certificate request in an X509Req object
"""
req = crypto.X509Req()
subj = req.get_subject()
for (key, value) in name.items():
setattr(subj, key, value)
req.set_pubkey(pkey)
req.sign(pkey, digest)
return req
def createCertificate(req, issuerCert, issuerKey, serial, notBefore, notAfter, digest="sha256"):
"""
Generate a certificate given a certificate request.
Arguments: req - Certificate request to use
issuerCert - The certificate of the issuer
issuerKey - The private key of the issuer
serial - Serial number for the certificate
notBefore - Timestamp (relative to now) when the certificate
starts being valid
notAfter - Timestamp (relative to now) when the certificate
stops being valid
digest - Digest method to use for signing, default is sha256
Returns: The signed certificate in an X509 object
"""
cert = crypto.X509()
cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(notBefore)
cert.gmtime_adj_notAfter(notAfter)
cert.set_issuer(issuerCert.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(issuerKey, digest)
return cert
def make_certs(filepath, CN):
"""
make self signed certificates
filepath : absolute path of certificate file, will be appended the '.pkey'
and '.cert' extensions
CN : common name
"""
key = createKeyPair(TYPE_RSA, 4096)
req = createCertRequest(key, CN=CN)
cert = createCertificate(req, req, key, 0, 0, 60*60*24*365*5) # five years
with open(filepath.with_suffix('.pkey'), 'wb') as f:
filepath.with_suffix('.pkey').chmod(0o600)
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
with open(filepath.with_suffix('.cert'), 'wb') as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
if __name__ == '__main__':
make_certs(Path('selfcert'), 'gajim')

View File

@ -0,0 +1,231 @@
# Copyright (C) 2009 Bruno Tarquini <btarquini AT gmail.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import sys
import time
from datetime import datetime
from gajim.common import app
from gajim.common import configpaths
from gajim.common.i18n import _
def parseLogLevel(arg):
"""
Either numeric value or level name from logging module
"""
if arg.isdigit():
return int(arg)
if arg.isupper() and hasattr(logging, arg):
return getattr(logging, arg)
print(_('%s is not a valid loglevel') % repr(arg), file=sys.stderr)
return 0
def parseLogTarget(arg):
"""
[gajim.]c.x.y -> gajim.c.x.y
.other_logger -> other_logger
<None> -> gajim
"""
arg = arg.lower()
if not arg:
return 'gajim'
if arg.startswith('.'):
return arg[1:]
if arg.startswith('gajim'):
return arg
return 'gajim.' + arg
def parseAndSetLogLevels(arg):
"""
[=]LOGLEVEL -> gajim=LOGLEVEL
gajim=LOGLEVEL -> gajim=LOGLEVEL
.other=10 -> other=10
.=10 -> <nothing>
c.x.y=c.z=20 -> gajim.c.x.y=20
gajim.c.z=20
gajim=10,c.x=20 -> gajim=10
gajim.c.x=20
"""
for directive in arg.split(','):
directive = directive.strip()
if not directive:
continue
if '=' not in directive:
directive = '=' + directive
targets, level = directive.rsplit('=', 1)
level = parseLogLevel(level.strip())
for target in targets.split('='):
target = parseLogTarget(target.strip())
if target:
logging.getLogger(target).setLevel(level)
print("Logger %s level set to %d" % (target, level),
file=sys.stderr)
class colors:
# pylint: disable=C0326
NONE = chr(27) + "[0m"
BLACk = chr(27) + "[30m"
RED = chr(27) + "[31m"
GREEN = chr(27) + "[32m"
BROWN = chr(27) + "[33m"
BLUE = chr(27) + "[34m"
MAGENTA = chr(27) + "[35m"
CYAN = chr(27) + "[36m"
LIGHT_GRAY = chr(27) + "[37m"
DARK_GRAY = chr(27) + "[30;1m"
BRIGHT_RED = chr(27) + "[31;1m"
BRIGHT_GREEN = chr(27) + "[32;1m"
YELLOW = chr(27) + "[33;1m"
BRIGHT_BLUE = chr(27) + "[34;1m"
PURPLE = chr(27) + "[35;1m"
BRIGHT_CYAN = chr(27) + "[36;1m"
WHITE = chr(27) + "[37;1m"
def colorize(text, color):
return color + text + colors.NONE
class FancyFormatter(logging.Formatter):
"""
An eye-candy formatter with colors
"""
colors_mapping = {
'DEBUG': colors.BLUE,
'INFO': colors.GREEN,
'WARNING': colors.BROWN,
'ERROR': colors.RED,
'CRITICAL': colors.BRIGHT_RED,
}
def __init__(self, fmt, datefmt=None, use_color=False):
logging.Formatter.__init__(self, fmt, datefmt)
self.use_color = use_color
def formatTime(self, record, datefmt=None):
f = logging.Formatter.formatTime(self, record, datefmt)
if self.use_color:
f = colorize(f, colors.DARK_GRAY)
return f
def format(self, record):
level = record.levelname
record.levelname = '(%s)' % level[0]
if self.use_color:
c = FancyFormatter.colors_mapping.get(level, '')
record.levelname = colorize(record.levelname, c)
record.name = '%-25s' % colorize(record.name, colors.CYAN)
else:
record.name = '%-25s|' % record.name
return logging.Formatter.format(self, record)
def init():
"""
Iinitialize the logging system
"""
if app.get_debug_mode():
_cleanup_debug_logs()
_redirect_output()
use_color = False
if os.name != 'nt':
use_color = sys.stderr.isatty()
consoleloghandler = logging.StreamHandler()
consoleloghandler.setFormatter(
FancyFormatter(
'%(asctime)s %(levelname)s %(name)-35s %(message)s',
'%x %H:%M:%S',
use_color
)
)
root_log = logging.getLogger('gajim')
root_log.setLevel(logging.WARNING)
root_log.addHandler(consoleloghandler)
root_log.propagate = False
root_log = logging.getLogger('nbxmpp')
root_log.setLevel(logging.WARNING)
root_log.addHandler(consoleloghandler)
root_log.propagate = False
root_log = logging.getLogger('gnupg')
root_log.setLevel(logging.WARNING)
root_log.addHandler(consoleloghandler)
root_log.propagate = False
# GAJIM_DEBUG is set only on Windows when using Gajim-Debug.exe
# Gajim-Debug.exe shows a command line prompt and we want to redirect
# log output to it
if app.get_debug_mode() or os.environ.get('GAJIM_DEBUG', False):
set_verbose()
def set_loglevels(loglevels_string):
parseAndSetLogLevels(loglevels_string)
def set_verbose():
parseAndSetLogLevels('gajim=DEBUG')
parseAndSetLogLevels('.nbxmpp=INFO')
def set_quiet():
parseAndSetLogLevels('gajim=CRITICAL')
parseAndSetLogLevels('.nbxmpp=CRITICAL')
def _redirect_output():
debug_folder = configpaths.get('DEBUG')
date = datetime.today().strftime('%d%m%Y-%H%M%S')
filename = '%s-debug.log' % date
fd = open(debug_folder / filename, 'a')
sys.stderr = sys.stdout = fd
def _cleanup_debug_logs():
debug_folder = configpaths.get('DEBUG')
debug_files = list(debug_folder.glob('*-debug.log*'))
now = time.time()
for file in debug_files:
# Delete everything older than 3 days
if file.stat().st_ctime < now - 259200:
file.unlink()
# tests
if __name__ == '__main__':
init()
set_loglevels('gajim.c=DEBUG,INFO')
log = logging.getLogger('gajim')
log.debug('debug')
log.info('info')
log.warning('warn')
log.error('error')
log.critical('critical')
log = logging.getLogger('gajim.c.x.dispatcher')
log.debug('debug')
log.info('info')
log.warning('warn')
log.error('error')
log.critical('critical')

View File

@ -0,0 +1,174 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Any
from typing import Dict # pylint: disable=unused-import
from typing import List
from typing import Tuple
import sys
import logging
from importlib import import_module
from unittest.mock import MagicMock
from gajim.common.types import ConnectionT
log = logging.getLogger('gajim.c.m')
ZEROCONF_MODULES = ['iq',
'adhoc_commands',
'receipts',
'discovery',
'chatstates']
MODULES = [
'adhoc_commands',
'annotations',
'bits_of_binary',
'blocking',
'bookmarks',
'caps',
'carbons',
'chat_markers',
'chatstates',
'delimiter',
'discovery',
'entity_time',
'gateway',
'httpupload',
'http_auth',
'iq',
'last_activity',
'mam',
'message',
'metacontacts',
'muc',
'pep',
'ping',
'presence',
'pubsub',
'receipts',
'register',
'roster',
'roster_item_exchange',
'search',
'security_labels',
'software_version',
'user_activity',
'user_avatar',
'user_location',
'user_mood',
'user_nickname',
'user_tune',
'vcard4',
'vcard_avatars',
'vcard_temp',
'announce',
'ibb',
'jingle',
'bytestream',
]
_imported_modules = [] # type: List[tuple]
_modules = {} # type: Dict[str, Dict[str, Any]]
_store_publish_modules = [
'UserMood',
'UserActivity',
'UserLocation',
'UserTune',
] # type: List[str]
class ModuleMock:
def __init__(self, name: str) -> None:
self._name = name
# HTTPUpload, ..
self.available = False
# Blocking
self.blocked = [] # type: List[Any]
# Delimiter
self.delimiter = '::'
# Bookmarks
self.bookmarks = {} # type: Dict[Any, Any]
# Various Modules
self.supported = False
def __getattr__(self, key: str) -> MagicMock:
return MagicMock()
def register_modules(con: ConnectionT, *args: Any, **kwargs: Any) -> None:
if con in _modules:
return
_modules[con.name] = {}
for module_name in MODULES:
if con.name == 'Local':
if module_name not in ZEROCONF_MODULES:
continue
instance, name = _load_module(module_name, con, *args, **kwargs)
_modules[con.name][name] = instance
def register_single_module(con: ConnectionT, instance: Any, name: str) -> None:
if con.name not in _modules:
raise ValueError('Unknown account name: %s' % con.name)
_modules[con.name][name] = instance
def unregister_modules(con: ConnectionT) -> None:
for instance in _modules[con.name].values():
if hasattr(instance, 'cleanup'):
instance.cleanup()
del _modules[con.name]
def unregister_single_module(con: ConnectionT, name: str) -> None:
if con.name not in _modules:
return
if name not in _modules[con.name]:
return
del _modules[con.name][name]
def send_stored_publish(account: str) -> None:
for name in _store_publish_modules:
_modules[account][name].send_stored_publish()
def get(account: str, name: str) -> Any:
try:
return _modules[account][name]
except KeyError:
return ModuleMock(name)
def _load_module(name: str, con: ConnectionT, *args: Any, **kwargs: Any) -> Any:
if name not in MODULES:
raise ValueError('Module %s does not exist' % name)
module = sys.modules.get(name)
if module is None:
module = import_module('.%s' % name, package='gajim.common.modules')
return module.get_instance(con, *args, **kwargs) # type: ignore
def get_handlers(con: ConnectionT) -> List[Tuple[Any, ...]]:
handlers = [] # type: List[Tuple[Any, ...]]
for module in _modules[con.name].values():
handlers += module.handlers
return handlers

View File

@ -0,0 +1,434 @@
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules import dataforms
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.modules.base import BaseModule
class AdHocCommand:
commandnode = 'command'
commandname = 'The Command'
commandfeatures = (Namespace.DATA,)
@staticmethod
def is_visible_for(_samejid):
"""
This returns True if that command should be visible and invocable for
others
samejid - True when command is invoked by an entity with the same bare
jid.
"""
return True
def __init__(self, conn, jid, sessionid):
self.connection = conn
self.jid = jid
self.sessionid = sessionid
def build_response(self, request, status='executing', defaultaction=None,
actions=None):
assert status in ('executing', 'completed', 'canceled')
response = request.buildReply('result')
cmd = response.getTag('command', namespace=Namespace.COMMANDS)
cmd.setAttr('sessionid', self.sessionid)
cmd.setAttr('node', self.commandnode)
cmd.setAttr('status', status)
if defaultaction is not None or actions is not None:
if defaultaction is not None:
assert defaultaction in ('cancel', 'execute', 'prev', 'next',
'complete')
attrs = {'action': defaultaction}
else:
attrs = {}
cmd.addChild('actions', attrs, actions)
return response, cmd
def bad_request(self, stanza):
self.connection.connection.send(
nbxmpp.Error(stanza, Namespace.STANZAS + ' bad-request'))
def cancel(self, request):
response = self.build_response(request, status='canceled')[0]
self.connection.connection.send(response)
return False # finish the session
class ChangeStatusCommand(AdHocCommand):
commandnode = 'change-status'
commandname = _('Change status information')
def __init__(self, conn, jid, sessionid):
AdHocCommand.__init__(self, conn, jid, sessionid)
self._callback = self.first_step
@staticmethod
def is_visible_for(samejid):
"""
Change status is visible only if the entity has the same bare jid
"""
return samejid
def execute(self, request):
return self._callback(request)
def first_step(self, request):
# first query...
response, cmd = self.build_response(request,
defaultaction='execute',
actions=['execute'])
cmd.addChild(
node=dataforms.SimpleDataForm(
title=_('Change status'),
instructions=_('Set the presence type and description'),
fields=[
dataforms.create_field(
'list-single',
var='presence-type',
label='Type of presence:',
options=[
('chat', _('Free for chat')),
('online', _('Online')),
('away', _('Away')),
('xa', _('Extended away')),
('dnd', _('Do not disturb')),
('offline', _('Offline - disconnect'))],
value='online',
required=True),
dataforms.create_field(
'text-multi',
var='presence-desc',
label=_('Presence description:'))
]
)
)
self.connection.connection.send(response)
# for next invocation
self._callback = self.second_step
return True # keep the session
def second_step(self, request):
# check if the data is correct
try:
form = dataforms.SimpleDataForm(
extend=request.getTag('command').getTag('x'))
except Exception:
self.bad_request(request)
return False
try:
presencetype = form['presence-type'].value
if presencetype not in ('chat', 'online', 'away',
'xa', 'dnd', 'offline'):
self.bad_request(request)
return False
except Exception:
# KeyError if there's no presence-type field in form or
# AttributeError if that field is of wrong type
self.bad_request(request)
return False
try:
presencedesc = form['presence-desc'].value
except Exception: # same exceptions as in last comment
presencedesc = ''
response, cmd = self.build_response(request, status='completed')
cmd.addChild('note', {}, _('The status has been changed.'))
# if going offline, we need to push response so it won't go into
# queue and disappear
self.connection.connection.send(response,
now=presencetype == 'offline')
# send new status
app.interface.roster.send_status(
self.connection.name, presencetype, presencedesc)
return False # finish the session
class AdHocCommands(BaseModule):
_nbxmpp_extends = 'AdHoc'
_nbxmpp_methods = [
'request_command_list',
'execute_command',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._execute_command_received,
typ='set',
ns=Namespace.COMMANDS),
]
# a list of all commands exposed: node -> command class
self._commands = {}
if app.settings.get('remote_commands'):
for cmdobj in (ChangeStatusCommand,):
self._commands[cmdobj.commandnode] = cmdobj
# a list of sessions; keys are tuples (jid, sessionid, node)
self._sessions = {}
def get_own_bare_jid(self):
return self._con.get_own_jid().bare
def is_same_jid(self, jid):
"""
Test if the bare jid given is the same as our bare jid
"""
return nbxmpp.JID.from_string(jid).bare == self.get_own_bare_jid()
def command_list_query(self, stanza):
iq = stanza.buildReply('result')
jid = helpers.get_full_jid_from_iq(stanza)
query = iq.getTag('query')
# buildReply don't copy the node attribute. Re-add it
query.setAttr('node', Namespace.COMMANDS)
for node, cmd in self._commands.items():
if cmd.is_visible_for(self.is_same_jid(jid)):
query.addChild('item', {
# TODO: find the jid
'jid': str(self._con.get_own_jid()),
'node': node,
'name': cmd.commandname})
self._con.connection.send(iq)
def command_info_query(self, stanza):
"""
Send disco#info result for query for command (XEP-0050, example 6.).
Return True if the result was sent, False if not
"""
try:
jid = helpers.get_full_jid_from_iq(stanza)
except helpers.InvalidFormat:
self._log.warning('Invalid JID: %s, ignoring it', stanza.getFrom())
return False
node = stanza.getTagAttr('query', 'node')
if node not in self._commands:
return False
cmd = self._commands[node]
if cmd.is_visible_for(self.is_same_jid(jid)):
iq = stanza.buildReply('result')
query = iq.getTag('query')
query.addChild('identity',
attrs={'type': 'command-node',
'category': 'automation',
'name': cmd.commandname})
query.addChild('feature', attrs={'var': Namespace.COMMANDS})
for feature in cmd.commandfeatures:
query.addChild('feature', attrs={'var': feature})
self._con.connection.send(iq)
return True
return False
def command_items_query(self, stanza):
"""
Send disco#items result for query for command.
Return True if the result was sent, False if not.
"""
jid = helpers.get_full_jid_from_iq(stanza)
node = stanza.getTagAttr('query', 'node')
if node not in self._commands:
return False
cmd = self._commands[node]
if cmd.is_visible_for(self.is_same_jid(jid)):
iq = stanza.buildReply('result')
self._con.connection.send(iq)
return True
return False
def _execute_command_received(self, _con, stanza, _properties):
jid = helpers.get_full_jid_from_iq(stanza)
cmd = stanza.getTag('command')
if cmd is None:
self._log.error('Malformed stanza (no command node) %s', stanza)
raise nbxmpp.NodeProcessed
node = cmd.getAttr('node')
if node is None:
self._log.error('Malformed stanza (no node attr) %s', stanza)
raise nbxmpp.NodeProcessed
sessionid = cmd.getAttr('sessionid')
if sessionid is None:
# we start a new command session
# only if we are visible for the jid and command exist
if node not in self._commands.keys():
self._con.connection.send(
nbxmpp.Error(
stanza, Namespace.STANZAS + ' item-not-found'))
self._log.warning('Comand %s does not exist: %s', node, jid)
raise nbxmpp.NodeProcessed
newcmd = self._commands[node]
if not newcmd.is_visible_for(self.is_same_jid(jid)):
self._log.warning('Command not visible for jid: %s', jid)
raise nbxmpp.NodeProcessed
# generate new sessionid
sessionid = generate_id()
# create new instance and run it
obj = newcmd(conn=self, jid=jid, sessionid=sessionid)
rc = obj.execute(stanza)
if rc:
self._sessions[(jid, sessionid, node)] = obj
self._log.info('Comand %s executed: %s', node, jid)
raise nbxmpp.NodeProcessed
# the command is already running, check for it
magictuple = (jid, sessionid, node)
if magictuple not in self._sessions:
# we don't have this session... ha!
self._log.warning('Invalid session %s', magictuple)
raise nbxmpp.NodeProcessed
action = cmd.getAttr('action')
obj = self._sessions[magictuple]
try:
if action == 'cancel':
rc = obj.cancel(stanza)
elif action == 'prev':
rc = obj.prev(stanza)
elif action == 'next':
rc = obj.next(stanza)
elif action == 'execute' or action is None:
rc = obj.execute(stanza)
elif action == 'complete':
rc = obj.complete(stanza)
else:
# action is wrong. stop the session, send error
raise AttributeError
except AttributeError:
# the command probably doesn't handle invoked action...
# stop the session, return error
del self._sessions[magictuple]
self._log.warning('Wrong action %s %s', node, jid)
raise nbxmpp.NodeProcessed
# delete the session if rc is False
if not rc:
del self._sessions[magictuple]
raise nbxmpp.NodeProcessed
def send_command(self, jid, node, session_id,
form, action='execute'):
"""
Send the command with data form. Wait for reply
"""
self._log.info('Send Command: %s %s %s %s',
jid, node, session_id, action)
stanza = nbxmpp.Iq(typ='set', to=jid)
cmdnode = stanza.addChild('command',
namespace=Namespace.COMMANDS,
attrs={'node': node,
'action': action})
if session_id:
cmdnode.setAttr('sessionid', session_id)
if form:
cmdnode.addChild(node=form.get_purged())
self._con.connection.SendAndCallForResponse(
stanza, self._action_response_received)
def _action_response_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event(
AdHocCommandError(None, conn=self._con,
error=stanza.getError()))
return
self._log.info('Received action response')
command = stanza.getTag('command')
app.nec.push_incoming_event(
AdHocCommandActionResponse(
None, conn=self._con, command=command))
def send_cancel(self, jid, node, session_id):
"""
Send the command with action='cancel'
"""
self._log.info('Cancel: %s %s %s', jid, node, session_id)
stanza = nbxmpp.Iq(typ='set', to=jid)
stanza.addChild('command', namespace=Namespace.COMMANDS,
attrs={
'node': node,
'sessionid': session_id,
'action': 'cancel'
})
self._con.connection.SendAndCallForResponse(
stanza, self._cancel_result_received)
def _cancel_result_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.warning('Error: %s', stanza.getError())
else:
self._log.info('Cancel successful')
class AdHocCommandError(NetworkIncomingEvent):
name = 'adhoc-command-error'
class AdHocCommandActionResponse(NetworkIncomingEvent):
name = 'adhoc-command-action-response'
def get_instance(*args, **kwargs):
return AdHocCommands(*args, **kwargs), 'AdHocCommands'

View File

@ -0,0 +1,66 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0145: Annotations
from typing import Any
from typing import Dict # pylint: disable=unused-import
from typing import Tuple
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.structs import AnnotationNote
from gajim.common.types import ConnectionT
from gajim.common.modules.base import BaseModule
class Annotations(BaseModule):
_nbxmpp_extends = 'Annotations'
_nbxmpp_methods = [
'request_annotations',
'set_annotations',
]
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self._annotations = {} # type: Dict[str, AnnotationNote]
def request_annotations(self) -> None:
self._nbxmpp('Annotations').request_annotations(
callback=self._annotations_received)
def _annotations_received(self, task: Any) -> None:
try:
annotations = task.finish()
except (StanzaError, MalformedStanzaError) as error:
self._log.warning(error)
self._annotations = {}
return
for note in annotations:
self._annotations[note.jid] = note
def get_note(self, jid: str) -> AnnotationNote:
return self._annotations.get(jid)
def set_note(self, note: AnnotationNote) -> None:
self._annotations[note.jid] = note
self.set_annotations(self._annotations.values())
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Annotations, str]:
return Annotations(*args, **kwargs), 'Annotations'

View File

@ -0,0 +1,37 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# Server MOTD and Announce
import nbxmpp
from gajim.common.modules.base import BaseModule
class Announce(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
def delete_motd(self):
server = self._con.get_own_jid().domain
jid = '%s/announce/motd/delete' % server
self.set_announce(jid)
def set_announce(self, jid, subject=None, body=None):
message = nbxmpp.Message(to=jid, body=body, subject=subject)
self._nbxmpp().send(message)
def get_instance(*args, **kwargs):
return Announce(*args, **kwargs), 'Announce'

View File

@ -0,0 +1,97 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
import logging
from functools import partial
from unittest.mock import Mock
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.nec import EventHelper
from gajim.common.modules.util import LogAdapter
class BaseModule(EventHelper):
_nbxmpp_extends = ''
_nbxmpp_methods = [] # type: List[str]
def __init__(self, con, *args, plugin=False, **kwargs):
EventHelper.__init__(self)
self._con = con
self._account = con.name
self._log = self._set_logger(plugin)
self._nbxmpp_callbacks = {} # type: Dict[str, Any]
self._stored_publish = None # type: Callable
self.handlers = [] # type: List[str]
def _set_logger(self, plugin):
logger_name = 'gajim.c.m.%s'
if plugin:
logger_name = 'gajim.p.%s'
logger_name = logger_name % self.__class__.__name__.lower()
logger = logging.getLogger(logger_name)
return LogAdapter(logger, {'account': self._account})
def __getattr__(self, key):
if key not in self._nbxmpp_methods:
raise AttributeError(
"attribute '%s' is neither part of object '%s' "
" nor declared in '_nbxmpp_methods'" % (
key, self.__class__.__name__))
if not app.account_is_connected(self._account):
self._log.warning('Account not connected, cant use %s', key)
return None
module = self._con.connection.get_module(self._nbxmpp_extends)
callback = self._nbxmpp_callbacks.get(key)
if callback is None:
return getattr(module, key)
return partial(getattr(module, key), callback=callback)
def _nbxmpp(self, module_name=None):
if not app.account_is_connected(self._account):
self._log.warning('Account not connected, cant use nbxmpp method')
return Mock()
if module_name is None:
return self._con.connection
return self._con.connection.get_module(module_name)
def _register_callback(self, method, callback):
self._nbxmpp_callbacks[method] = callback
def _register_pubsub_handler(self, callback):
handler = StanzaHandler(name='message',
callback=callback,
ns=Namespace.PUBSUB_EVENT,
priority=49)
self.handlers.append(handler)
def send_stored_publish(self):
if self._stored_publish is None:
return
self._log.info('Send stored publish')
self._stored_publish() # pylint: disable=not-callable
def cleanup(self):
self.unregister_events()

View File

@ -0,0 +1,204 @@
# Copyright (C) 2018 Emmanuel Gil Peyrot <linkmauve AT linkmauve.fr>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import hashlib
from base64 import b64decode
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common import configpaths
from gajim.common.modules.base import BaseModule
log = logging.getLogger('gajim.c.m.bob')
class BitsOfBinary(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._answer_bob_request,
typ='get',
ns=Namespace.BOB),
]
# Used to track which cids are in-flight.
self.awaiting_cids = {}
def _answer_bob_request(self, _con, stanza, _properties):
self._log.info('Request from %s for BoB data', stanza.getFrom())
iq = stanza.buildReply('error')
err = nbxmpp.ErrorNode(nbxmpp.ERR_ITEM_NOT_FOUND)
iq.addChild(node=err)
self._log.info('Sending item-not-found')
self._con.connection.send(iq)
raise nbxmpp.NodeProcessed
def _on_bob_received(self, _nbxmpp_client, result, cid):
"""
Called when we receive BoB data
"""
if cid not in self.awaiting_cids:
return
if result.getType() == 'result':
data = result.getTags('data', namespace=Namespace.BOB)
if data.getAttr('cid') == cid:
for func in self.awaiting_cids[cid]:
cb = func[0]
args = func[1]
pos = func[2]
bob_data = data.getData()
def recurs(node, cid, data):
if node.getData() == 'cid:' + cid:
node.setData(data)
else:
for child in node.getChildren():
recurs(child, cid, data)
recurs(args[pos], cid, bob_data)
cb(*args)
del self.awaiting_cids[cid]
return
# An error occurred, call callback without modifying data.
for func in self.awaiting_cids[cid]:
cb = func[0]
args = func[1]
cb(*args)
del self.awaiting_cids[cid]
def get_bob_data(self, cid, to, callback, args, position):
"""
Request for BoB (XEP-0231) and when data will arrive, call callback
with given args, after having replaced cid by it's data in
args[position]
"""
if cid in self.awaiting_cids:
self.awaiting_cids[cid].appends((callback, args, position))
else:
self.awaiting_cids[cid] = [(callback, args, position)]
iq = nbxmpp.Iq(to=to, typ='get')
iq.addChild(name='data', attrs={'cid': cid}, namespace=Namespace.BOB)
self._con.connection.SendAndCallForResponse(
iq, self._on_bob_received, {'cid': cid})
def parse_bob_data(stanza):
data_node = stanza.getTag('data', namespace=Namespace.BOB)
if data_node is None:
return None
cid = data_node.getAttr('cid')
type_ = data_node.getAttr('type')
max_age = data_node.getAttr('max-age')
if max_age is not None:
try:
max_age = int(max_age)
except Exception:
log.exception(stanza)
return None
if cid is None or type_ is None:
log.warning('Invalid data node (no cid or type attr): %s', stanza)
return None
try:
algo_hash = cid.split('@')[0]
algo, hash_ = algo_hash.split('+')
except Exception:
log.exception('Invalid cid: %s', stanza)
return None
bob_data = data_node.getData()
if not bob_data:
log.warning('No data found: %s', stanza)
return None
filepath = configpaths.get('BOB') / algo_hash
if algo_hash in app.bob_cache or filepath.exists():
log.info('BoB data already cached')
return None
try:
bob_data = b64decode(bob_data)
except Exception:
log.warning('Unable to decode data')
log.exception(stanza)
return None
if len(bob_data) > 10000:
log.warning('%s: data > 10000 bytes', stanza.getFrom())
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
if max_age == 0:
app.bob_cache[algo_hash] = bob_data
else:
try:
with open(str(filepath), 'w+b') as file:
file.write(bob_data)
except Exception:
log.warning('Unable to save data')
log.exception(stanza)
return None
log.info('BoB data stored: %s', algo_hash)
return filepath
def store_bob_data(bob_data):
if bob_data is None:
return None
algo_hash = '%s+%s' % (bob_data.algo, bob_data.hash_)
filepath = configpaths.get('BOB') / algo_hash
if algo_hash in app.bob_cache or filepath.exists():
log.info('BoB data already cached')
return None
if bob_data.max_age == 0:
app.bob_cache[algo_hash] = bob_data.data
else:
try:
with open(str(filepath), 'w+b') as file:
file.write(bob_data.data)
except Exception:
log.exception('Unable to save data')
return None
log.info('BoB data stored: %s', algo_hash)
return filepath
def get_instance(*args, **kwargs):
return BitsOfBinary(*args, **kwargs), 'BitsOfBinary'

View File

@ -0,0 +1,139 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0191: Blocking Command
import nbxmpp
from nbxmpp.protocol import JID
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.util import raise_if_error
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class Blocking(BaseModule):
_nbxmpp_extends = 'Blocking'
_nbxmpp_methods = [
'block',
'unblock',
'request_blocking_list',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.blocked = []
self.handlers = [
StanzaHandler(name='iq',
callback=self._blocking_push_received,
typ='set',
ns=Namespace.BLOCKING),
]
self.supported = False
def pass_disco(self, info):
if Namespace.BLOCKING not in info.features:
return
self.supported = True
app.nec.push_incoming_event(
NetworkEvent('feature-discovered',
account=self._account,
feature=Namespace.BLOCKING))
self._log.info('Discovered blocking: %s', info.jid)
@as_task
def get_blocking_list(self):
_task = yield
blocking_list = yield self._nbxmpp('Blocking').request_blocking_list()
raise_if_error(blocking_list)
self.blocked = list(blocking_list)
app.nec.push_incoming_event(NetworkEvent('blocking',
conn=self._con,
changed=self.blocked))
yield blocking_list
@as_task
def update_blocking_list(self, block, unblock):
_task = yield
if block:
result = yield self.block(block)
raise_if_error(result)
if unblock:
result = yield self.unblock(unblock)
raise_if_error(result)
yield True
def _blocking_push_received(self, _con, _stanza, properties):
if not properties.is_blocking:
return
changed_list = []
if properties.blocking.unblock_all:
self.blocked = []
for jid in self.blocked:
self._presence_probe(jid)
self._log.info('Unblock all Push')
for jid in properties.blocking.unblock:
changed_list.append(jid)
if jid not in self.blocked:
continue
self.blocked.remove(jid)
self._presence_probe(jid)
self._log.info('Unblock Push: %s', jid)
for jid in properties.blocking.block:
if jid in self.blocked:
continue
changed_list.append(jid)
self.blocked.append(jid)
self._set_contact_offline(str(jid))
self._log.info('Block Push: %s', jid)
app.nec.push_incoming_event(NetworkEvent('blocking',
conn=self._con,
changed=changed_list))
raise nbxmpp.NodeProcessed
def _set_contact_offline(self, jid: str) -> None:
contact_list = app.contacts.get_contacts(self._account, jid)
for contact in contact_list:
contact.show = 'offline'
def _presence_probe(self, jid: JID) -> None:
self._log.info('Presence probe: %s', jid)
# Send a presence Probe to get the current Status
probe = nbxmpp.Presence(jid, 'probe', frm=self._con.get_own_jid())
self._nbxmpp().send(probe)
def get_instance(*args, **kwargs):
return Blocking(*args, **kwargs), 'Blocking'

View File

@ -0,0 +1,353 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0048: Bookmarks
from typing import Any
from typing import List
from typing import Dict
from typing import Set
from typing import Tuple
from typing import Union
from typing import Optional
import functools
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.structs import BookmarkData
from gi.repository import GLib
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
NODE_MAX_NS = 'http://jabber.org/protocol/pubsub#config-node-max'
class Bookmarks(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._bookmark_event_received)
self._register_pubsub_handler(self._bookmark_1_event_received)
self._conversion = False
self._compat = False
self._compat_pep = False
self._node_max = False
self._bookmarks = {}
self._join_timeouts = []
self._request_in_progress = True
@property
def conversion(self) -> bool:
return self._conversion
@property
def compat(self) -> bool:
return self._compat
@property
def compat_pep(self) -> bool:
return self._compat_pep
@property
def bookmarks(self) -> List[BookmarkData]:
return self._bookmarks.values()
@property
def pep_bookmarks_used(self) -> bool:
return self._bookmark_module() == 'PEPBookmarks'
@property
def nativ_bookmarks_used(self) -> bool:
return self._bookmark_module() == 'NativeBookmarks'
@event_node(Namespace.BOOKMARKS)
def _bookmark_event_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
if not properties.is_self_message:
self._log.warning('%s has an open access bookmarks node',
properties.jid)
return
if not self.pep_bookmarks_used:
return
if self._request_in_progress:
self._log.info('Ignore update, pubsub request in progress')
return
bookmarks = self._convert_to_dict(properties.pubsub_event.data)
old_bookmarks = self._bookmarks.copy()
self._bookmarks = bookmarks
self._act_on_changed_bookmarks(old_bookmarks)
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
@event_node(Namespace.BOOKMARKS_1)
def _bookmark_1_event_received(self, _con, _stanza, properties):
if not properties.is_self_message:
self._log.warning('%s has an open access bookmarks node',
properties.jid)
return
if not self.nativ_bookmarks_used:
return
if self._request_in_progress:
self._log.info('Ignore update, pubsub request in progress')
return
old_bookmarks = self._bookmarks.copy()
if properties.pubsub_event.deleted or properties.pubsub_event.purged:
self._log.info('Bookmark node deleted/purged')
self._bookmarks = {}
elif properties.pubsub_event.retracted:
jid = properties.pubsub_event.id
self._log.info('Retract: %s', jid)
bookmark = self._bookmarks.get(jid)
if bookmark is not None:
self._bookmarks.pop(bookmark, None)
else:
new_bookmark = properties.pubsub_event.data
self._bookmarks[new_bookmark.jid] = properties.pubsub_event.data
self._act_on_changed_bookmarks(old_bookmarks)
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
def pass_disco(self, info):
self._node_max = NODE_MAX_NS in info.features
self._compat_pep = Namespace.BOOKMARKS_COMPAT_PEP in info.features
self._compat = Namespace.BOOKMARKS_COMPAT in info.features
self._conversion = Namespace.BOOKMARK_CONVERSION in info.features
@functools.lru_cache(maxsize=1)
def _bookmark_module(self):
if not self._con.get_module('PubSub').publish_options:
return 'PrivateBookmarks'
if app.settings.get('dev_force_bookmark_2'):
return 'NativeBookmarks'
if self._compat_pep and self._node_max:
return 'NativeBookmarks'
if self._conversion:
return 'PEPBookmarks'
return 'PrivateBookmarks'
def _act_on_changed_bookmarks(
self, old_bookmarks: Dict[str, BookmarkData]) -> None:
new_bookmarks = self._convert_to_set(self._bookmarks)
old_bookmarks = self._convert_to_set(old_bookmarks)
changed = new_bookmarks - old_bookmarks
if not changed:
return
join = [jid for jid, autojoin in changed if autojoin]
bookmarks = []
for jid in join:
self._log.info('Schedule autojoin in 10s for: %s', jid)
bookmarks.append(self._bookmarks.get(jid))
# If another client creates a MUC, the MUC is locked until the
# configuration is finished. Give the user some time to finish
# the configuration.
timeout_id = GLib.timeout_add_seconds(
10, self._join_with_timeout, bookmarks)
self._join_timeouts.append(timeout_id)
# TODO: leave mucs
# leave = [jid for jid, autojoin in changed if not autojoin]
@staticmethod
def _convert_to_set(
bookmarks: Dict[str, BookmarkData]) -> Set[Tuple[str, bool]]:
set_ = set()
for jid, bookmark in bookmarks.items():
set_.add((jid, bookmark.autojoin))
return set_
@staticmethod
def _convert_to_dict(bookmarks: List) -> Dict[str, BookmarkData]:
_dict = {} # type: Dict[str, BookmarkData]
if bookmarks is None:
return _dict
for bookmark in bookmarks:
_dict[bookmark.jid] = bookmark
return _dict
def get_bookmark(self, jid: Union[str, JID]) -> BookmarkData:
return self._bookmarks.get(jid)
def request_bookmarks(self) -> None:
if not app.account_is_available(self._account):
return
self._request_in_progress = True
self._nbxmpp(self._bookmark_module()).request_bookmarks(
callback=self._bookmarks_received)
def _bookmarks_received(self, task: Any) -> None:
try:
bookmarks = task.finish()
except Exception as error:
self._log.warning(error)
bookmarks = None
self._request_in_progress = False
self._bookmarks = self._convert_to_dict(bookmarks)
self.auto_join_bookmarks()
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
def store_difference(self, bookmarks: List) -> None:
if self.nativ_bookmarks_used:
retract, add_or_modify = self._determine_changed_bookmarks(
bookmarks, self._bookmarks)
for bookmark in retract:
self.remove(bookmark.jid)
if add_or_modify:
self.store_bookmarks(add_or_modify)
self._bookmarks = self._convert_to_dict(bookmarks)
else:
self._bookmarks = self._convert_to_dict(bookmarks)
self.store_bookmarks()
def store_bookmarks(self, bookmarks: list = None) -> None:
if not app.account_is_available(self._account):
return
if bookmarks is None or not self.nativ_bookmarks_used:
bookmarks = self._bookmarks.values()
self._nbxmpp(self._bookmark_module()).store_bookmarks(bookmarks)
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
def _join_with_timeout(self, bookmarks: List[Any]) -> None:
self._join_timeouts.pop(0)
self.auto_join_bookmarks(bookmarks)
def auto_join_bookmarks(self,
bookmarks: Optional[List[Any]] = None) -> None:
if bookmarks is None:
bookmarks = self._bookmarks.values()
for bookmark in bookmarks:
if bookmark.autojoin:
# Only join non-opened groupchats. Opened one are already
# auto-joined on re-connection
if bookmark.jid not in app.gc_connected[self._account]:
# we are not already connected
self._log.info('Autojoin Bookmark: %s', bookmark.jid)
minimize = app.settings.get_group_chat_setting(
self._account,
bookmark.jid,
'minimize_on_autojoin')
app.interface.join_groupchat(self._account,
str(bookmark.jid),
minimized=minimize)
def modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
bookmark = self._bookmarks.get(jid)
if bookmark is None:
return
new_bookmark = bookmark._replace(**kwargs)
if new_bookmark == bookmark:
# No change happened
return
self._log.info('Modify bookmark: %s %s', jid, kwargs)
self._bookmarks[jid] = new_bookmark
self.store_bookmarks([new_bookmark])
def add_or_modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
bookmark = self._bookmarks.get(jid)
if bookmark is not None:
self.modify(jid, **kwargs)
return
new_bookmark = BookmarkData(jid=jid, **kwargs)
self._bookmarks[jid] = new_bookmark
self._log.info('Add new bookmark: %s', new_bookmark)
self.store_bookmarks([new_bookmark])
def remove(self, jid: JID, publish: bool = True) -> None:
removed = self._bookmarks.pop(jid, False)
if not removed:
return
if publish:
if self.nativ_bookmarks_used:
self._nbxmpp('NativeBookmarks').retract_bookmark(str(jid))
else:
self.store_bookmarks()
@staticmethod
def _determine_changed_bookmarks(
new_bookmarks: List[BookmarkData],
old_bookmarks: Dict[str, BookmarkData]) -> Tuple[
List[BookmarkData], List[BookmarkData]]:
new_jids = [bookmark.jid for bookmark in new_bookmarks]
new_bookmarks = set(new_bookmarks)
old_bookmarks = set(old_bookmarks.values())
retract = []
add_or_modify = []
changed_bookmarks = new_bookmarks.symmetric_difference(old_bookmarks)
for bookmark in changed_bookmarks:
if bookmark.jid not in new_jids:
retract.append(bookmark)
if bookmark in new_bookmarks:
add_or_modify.append(bookmark)
return retract, add_or_modify
def get_name_from_bookmark(self, jid: str) -> str:
bookmark = self._bookmarks.get(jid)
if bookmark is None:
return ''
return bookmark.name
def is_bookmark(self, jid: str) -> bool:
return jid in self._bookmarks
def _remove_timeouts(self):
for _id in self._join_timeouts:
GLib.source_remove(_id)
def cleanup(self):
self._remove_timeouts()
def get_instance(*args, **kwargs):
return Bookmarks(*args, **kwargs), 'Bookmarks'

View File

@ -0,0 +1,719 @@
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Junglecow J <junglecow AT gmail.com>
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Jean-Marie Traissard <jim AT lapin.org>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import socket
import logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gi.repository import GLib
from gajim.common import app
from gajim.common import helpers
from gajim.common import jingle_xtls
from gajim.common.file_props import FilesProp
from gajim.common.socks5 import Socks5SenderClient
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
log = logging.getLogger('gajim.c.m.bytestream')
def is_transfer_paused(file_props):
if file_props.stopped:
return False
if file_props.completed:
return False
if file_props.disconnect_cb:
return False
return file_props.paused
def is_transfer_active(file_props):
if file_props.stopped:
return False
if file_props.completed:
return False
if not file_props.started:
return False
if file_props.paused:
return True
return not file_props.paused
def is_transfer_stopped(file_props):
if not file_props:
return True
if file_props.error:
return True
if file_props.completed:
return True
if not file_props.stopped:
return False
return True
class Bytestream(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
typ='result',
ns=Namespace.BYTESTREAM,
callback=self._on_bytestream_result),
StanzaHandler(name='iq',
typ='error',
ns=Namespace.BYTESTREAM,
callback=self._on_bytestream_error),
StanzaHandler(name='iq',
typ='set',
ns=Namespace.BYTESTREAM,
callback=self._on_bytestream_set),
StanzaHandler(name='iq',
typ='result',
callback=self._on_result),
]
self.no_gupnp_reply_id = None
self.ok_id = None
self.fail_id = None
def pass_disco(self, info):
if Namespace.BYTESTREAM not in info.features:
return
if app.settings.get_account_setting(self._account, 'use_ft_proxies'):
log.info('Discovered proxy: %s', info.jid)
our_fjid = self._con.get_own_jid()
testit = app.settings.get_account_setting(
self._account, 'test_ft_proxies_on_startup')
app.proxy65_manager.resolve(
info.jid, self._con.connection, str(our_fjid),
default=self._account, testit=testit)
raise nbxmpp.NodeProcessed
def _ft_get_receiver_jid(self, file_props):
if self._account == 'Local':
return file_props.receiver.jid
return file_props.receiver.jid + '/' + file_props.receiver.resource
def _ft_get_from(self, iq_obj):
if self._account == 'Local':
return iq_obj.getFrom()
return helpers.get_full_jid_from_iq(iq_obj)
def _ft_get_streamhost_jid_attr(self, streamhost):
if self._account == 'Local':
return streamhost.getAttr('jid')
return helpers.parse_jid(streamhost.getAttr('jid'))
def send_file_approval(self, file_props):
"""
Send iq, confirming that we want to download the file
"""
# user response to ConfirmationDialog may come after we've disconnected
if not app.account_is_available(self._account):
return
# file transfer initiated by a jingle session
log.info("send_file_approval: jingle session accept")
session = self._con.get_module('Jingle').get_jingle_session(
file_props.sender, file_props.sid)
if not session:
return
content = None
for content_ in session.contents.values():
if content_.transport.sid == file_props.transport_sid:
content = content_
break
if not content:
return
if not session.accepted:
content = session.get_content('file', content.name)
if content.use_security:
fingerprint = content.x509_fingerprint
if not jingle_xtls.check_cert(
app.get_jid_without_resource(file_props.sender),
fingerprint):
id_ = jingle_xtls.send_cert_request(
self._con, file_props.sender)
jingle_xtls.key_exchange_pend(id_,
content.on_cert_received, [])
return
session.approve_session()
session.approve_content('file', content.name)
def send_file_rejection(self, file_props):
"""
Inform sender that we refuse to download the file
typ is used when code = '400', in this case typ can be 'stream' for
invalid stream or 'profile' for invalid profile
"""
# user response to ConfirmationDialog may come after we've disconnected
if not app.account_is_available(self._account):
return
for session in self._con.get_module('Jingle').get_jingle_sessions(
None, file_props.sid):
session.cancel_session()
def send_success_connect_reply(self, streamhost):
"""
Send reply to the initiator of FT that we made a connection
"""
if not app.account_is_available(self._account):
return
if streamhost is None:
return
iq = nbxmpp.Iq(to=streamhost['initiator'],
typ='result',
frm=streamhost['target'])
iq.setAttr('id', streamhost['id'])
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
stream_tag = query.setTag('streamhost-used')
stream_tag.setAttr('jid', streamhost['jid'])
self._con.connection.send(iq)
def stop_all_active_file_transfers(self, contact):
"""
Stop all active transfer to or from the given contact
"""
for file_props in FilesProp.getAllFileProp():
if is_transfer_stopped(file_props):
continue
receiver_jid = file_props.receiver
if contact.get_full_jid() == receiver_jid:
file_props.error = -5
self.remove_transfer(file_props)
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(contact.jid),
file_props=file_props,
error_msg=''))
sender_jid = file_props.sender
if contact.get_full_jid() == sender_jid:
file_props.error = -3
self.remove_transfer(file_props)
def remove_all_transfers(self):
"""
Stop and remove all active connections from the socks5 pool
"""
for file_props in FilesProp.getAllFileProp():
self.remove_transfer(file_props)
def remove_transfer(self, file_props):
if file_props is None:
return
self.disconnect_transfer(file_props)
@staticmethod
def disconnect_transfer(file_props):
if file_props is None:
return
if file_props.hash_:
app.socks5queue.remove_sender(file_props.hash_)
if file_props.streamhosts:
for host in file_props.streamhosts:
if 'idx' in host and host['idx'] > 0:
app.socks5queue.remove_receiver(host['idx'])
app.socks5queue.remove_sender(host['idx'])
def _send_socks5_info(self, file_props):
"""
Send iq for the present streamhosts and proxies
"""
if not app.account_is_available(self._account):
return
receiver = file_props.receiver
sender = file_props.sender
sha_str = helpers.get_auth_sha(file_props.sid, sender, receiver)
file_props.sha_str = sha_str
port = app.settings.get('file_transfers_port')
listener = app.socks5queue.start_listener(
port,
sha_str,
self._result_socks5_sid, file_props)
if not listener:
file_props.error = -5
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(receiver),
file_props=file_props,
error_msg=''))
self._connect_error(file_props.sid,
error='not-acceptable',
error_type='modify')
else:
iq = nbxmpp.Iq(to=receiver, typ='set')
file_props.request_id = 'id_' + file_props.sid
iq.setID(file_props.request_id)
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
query.setAttr('sid', file_props.sid)
self._add_addiditional_streamhosts_to_query(query, file_props)
self._add_local_ips_as_streamhosts_to_query(query, file_props)
self._add_proxy_streamhosts_to_query(query, file_props)
self._add_upnp_igd_as_streamhost_to_query(query, file_props, iq)
# Upnp-igd is asynchronous, so it will send the iq itself
@staticmethod
def _add_streamhosts_to_query(query, sender, port, hosts):
for host in hosts:
streamhost = nbxmpp.Node(tag='streamhost')
query.addChild(node=streamhost)
streamhost.setAttr('port', str(port))
streamhost.setAttr('host', host)
streamhost.setAttr('jid', sender)
def _add_local_ips_as_streamhosts_to_query(self, query, file_props):
if not app.settings.get_account_setting(self._account,
'ft_send_local_ips'):
return
my_ip = self._con.local_address
if my_ip is None:
log.warning('No local address available')
return
try:
# The ip we're connected to server with
my_ips = [my_ip]
# all IPs from local DNS
for addr in socket.getaddrinfo(socket.gethostname(), None):
if (not addr[4][0] in my_ips and
not addr[4][0].startswith('127') and
not addr[4][0] == '::1'):
my_ips.append(addr[4][0])
sender = file_props.sender
port = app.settings.get('file_transfers_port')
self._add_streamhosts_to_query(query, sender, port, my_ips)
except socket.gaierror:
from gajim.common.connection_handlers_events import InformationEvent
app.nec.push_incoming_event(
InformationEvent(None, dialog_name='wrong-host'))
def _add_addiditional_streamhosts_to_query(self, query, file_props):
sender = file_props.sender
port = app.settings.get('file_transfers_port')
ft_add_hosts_to_send = app.settings.get('ft_add_hosts_to_send')
add_hosts = []
if ft_add_hosts_to_send:
add_hosts = [e.strip() for e in ft_add_hosts_to_send.split(',')]
else:
add_hosts = []
self._add_streamhosts_to_query(query, sender, port, add_hosts)
def _add_upnp_igd_as_streamhost_to_query(self, query, file_props, iq):
my_ip = self._con.local_address
if my_ip is None or not app.is_installed('UPNP'):
log.warning('No local address available')
self._con.connection.send(iq)
return
# check if we are connected with an IPv4 address
try:
socket.inet_aton(my_ip)
except socket.error:
self._con.connection.send(iq)
return
def ip_is_local(ip):
if '.' not in ip:
# it's an IPv6
return True
ip_s = ip.split('.')
ip_l = int(ip_s[0])<<24 | int(ip_s[1])<<16 | int(ip_s[2])<<8 | \
int(ip_s[3])
# 10/8
if ip_l & (255<<24) == 10<<24:
return True
# 172.16/12
if ip_l & (255<<24 | 240<<16) == (172<<24 | 16<<16):
return True
# 192.168
if ip_l & (255<<24 | 255<<16) == (192<<24 | 168<<16):
return True
return False
if not ip_is_local(my_ip):
self.connection.send(iq)
return
self.no_gupnp_reply_id = 0
def cleanup_gupnp():
if self.no_gupnp_reply_id:
GLib.source_remove(self.no_gupnp_reply_id)
self.no_gupnp_reply_id = 0
app.gupnp_igd.disconnect(self.ok_id)
app.gupnp_igd.disconnect(self.fail_id)
def success(_gupnp, _proto, ext_ip, _re, ext_port,
local_ip, local_port, _desc):
log.debug('Got GUPnP-IGD answer: external: %s:%s, internal: %s:%s',
ext_ip, ext_port, local_ip, local_port)
if local_port != app.settings.get('file_transfers_port'):
sender = file_props.sender
receiver = file_props.receiver
sha_str = helpers.get_auth_sha(file_props.sid,
sender,
receiver)
listener = app.socks5queue.start_listener(
local_port,
sha_str,
self._result_socks5_sid,
file_props.sid)
if listener:
self._add_streamhosts_to_query(query,
sender,
ext_port,
[ext_ip])
else:
self._add_streamhosts_to_query(query,
file_props.sender,
ext_port,
[ext_ip])
self._con.connection.send(iq)
cleanup_gupnp()
def fail(_gupnp, error, _proto, _ext_ip, _local_ip, _local_port, _desc):
log.debug('Got GUPnP-IGD error: %s', error)
self._con.connection.send(iq)
cleanup_gupnp()
def no_upnp_reply():
log.debug('Got not GUPnP-IGD answer')
# stop trying to use it
app.disable_dependency('UPNP')
self.no_gupnp_reply_id = 0
self._con.connection.send(iq)
cleanup_gupnp()
return False
self.ok_id = app.gupnp_igd.connect('mapped-external-port', success)
self.fail_id = app.gupnp_igd.connect('error-mapping-port', fail)
port = app.settings.get('file_transfers_port')
self.no_gupnp_reply_id = GLib.timeout_add_seconds(10, no_upnp_reply)
app.gupnp_igd.add_port('TCP',
0,
my_ip,
port,
3600,
'Gajim file transfer')
def _add_proxy_streamhosts_to_query(self, query, file_props):
proxyhosts = self._get_file_transfer_proxies_from_config(file_props)
if proxyhosts:
file_props.proxy_receiver = file_props.receiver
file_props.proxy_sender = file_props.sender
file_props.proxyhosts = proxyhosts
for proxyhost in proxyhosts:
self._add_streamhosts_to_query(query,
proxyhost['jid'],
proxyhost['port'],
[proxyhost['host']])
def _get_file_transfer_proxies_from_config(self, file_props):
configured_proxies = app.settings.get_account_setting(
self._account, 'file_transfer_proxies')
shall_use_proxies = app.settings.get_account_setting(
self._account, 'use_ft_proxies')
if shall_use_proxies:
proxyhost_dicts = []
proxies = []
if configured_proxies:
proxies = [item.strip() for item in
configured_proxies.split(',')]
default_proxy = app.proxy65_manager.get_default_for_name(
self._account)
if default_proxy:
# add/move default proxy at top of the others
if default_proxy in proxies:
proxies.remove(default_proxy)
proxies.insert(0, default_proxy)
for proxy in proxies:
(host, _port, jid) = app.proxy65_manager.get_proxy(
proxy, self._account)
if not host:
continue
host_dict = {
'state': 0,
'target': file_props.receiver,
'id': file_props.sid,
'sid': file_props.sid,
'initiator': proxy,
'host': host,
'port': str(_port),
'jid': jid
}
proxyhost_dicts.append(host_dict)
return proxyhost_dicts
return []
@staticmethod
def _result_socks5_sid(sid, hash_id):
"""
Store the result of SHA message from auth
"""
file_props = FilesProp.getFilePropBySid(sid)
file_props.hash_ = hash_id
def _connect_error(self, sid, error, error_type, msg=None):
"""
Called when there is an error establishing BS connection, or when
connection is rejected
"""
if not app.account_is_available(self._account):
return
file_props = FilesProp.getFileProp(self._account, sid)
if file_props is None:
log.error('can not send iq error on failed transfer')
return
if file_props.type_ == 's':
to = file_props.receiver
else:
to = file_props.sender
iq = nbxmpp.Iq(to=to, typ='error')
iq.setAttr('id', file_props.request_id)
err = iq.setTag('error')
err.setAttr('type', error_type)
err.setTag(error, namespace=Namespace.STANZAS)
self._con.connection.send(iq)
if msg:
self.disconnect_transfer(file_props)
file_props.error = -3
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(to),
file_props=file_props,
error_msg=msg))
def _proxy_auth_ok(self, proxy):
"""
Called after authentication to proxy server
"""
if not app.account_is_available(self._account):
return
file_props = FilesProp.getFileProp(self._account, proxy['sid'])
iq = nbxmpp.Iq(to=proxy['initiator'], typ='set')
auth_id = "au_" + proxy['sid']
iq.setID(auth_id)
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
query.setAttr('sid', proxy['sid'])
activate = query.setTag('activate')
activate.setData(file_props.proxy_receiver)
iq.setID(auth_id)
self._con.connection.send(iq)
def _on_bytestream_error(self, _con, iq_obj, _properties):
id_ = iq_obj.getAttr('id')
frm = helpers.get_full_jid_from_iq(iq_obj)
query = iq_obj.getTag('query')
app.proxy65_manager.error_cb(frm, query)
jid = helpers.get_jid_from_iq(iq_obj)
id_ = id_[3:]
file_props = FilesProp.getFilePropBySid(id_)
if not file_props:
return
file_props.error = -4
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(jid),
file_props=file_props,
error_msg=''))
raise nbxmpp.NodeProcessed
def _on_bytestream_set(self, con, iq_obj, _properties):
target = iq_obj.getAttr('to')
id_ = iq_obj.getAttr('id')
query = iq_obj.getTag('query')
sid = query.getAttr('sid')
file_props = FilesProp.getFileProp(self._account, sid)
streamhosts = []
for item in query.getChildren():
if item.getName() == 'streamhost':
host_dict = {
'state': 0,
'target': target,
'id': id_,
'sid': sid,
'initiator': self._ft_get_from(iq_obj)
}
for attr in item.getAttrs():
host_dict[attr] = item.getAttr(attr)
if 'host' not in host_dict:
continue
if 'jid' not in host_dict:
continue
if 'port' not in host_dict:
continue
streamhosts.append(host_dict)
file_props = FilesProp.getFilePropBySid(sid)
if file_props is not None:
if file_props.type_ == 's': # FIXME: remove fast xmlns
# only psi do this
if file_props.streamhosts:
file_props.streamhosts.extend(streamhosts)
else:
file_props.streamhosts = streamhosts
app.socks5queue.connect_to_hosts(
self._account,
sid,
self.send_success_connect_reply,
None)
raise nbxmpp.NodeProcessed
else:
log.warning('Gajim got streamhosts for unknown transfer. '
'Ignoring it.')
raise nbxmpp.NodeProcessed
file_props.streamhosts = streamhosts
def _connection_error(sid):
self._connect_error(sid,
'item-not-found',
'cancel',
msg='Could not connect to given hosts')
if file_props.type_ == 'r':
app.socks5queue.connect_to_hosts(
self._account,
sid,
self.send_success_connect_reply,
_connection_error)
raise nbxmpp.NodeProcessed
def _on_result(self, _con, iq_obj, _properties):
# if we want to respect xep-0065 we have to check for proxy
# activation result in any result iq
real_id = iq_obj.getAttr('id')
if real_id is None:
log.warning('Invalid IQ without id attribute:\n%s', iq_obj)
raise nbxmpp.NodeProcessed
if real_id is None or not real_id.startswith('au_'):
return
frm = self._ft_get_from(iq_obj)
id_ = real_id[3:]
file_props = FilesProp.getFilePropByTransportSid(self._account, id_)
if file_props.streamhost_used:
for host in file_props.proxyhosts:
if host['initiator'] == frm and 'idx' in host:
app.socks5queue.activate_proxy(host['idx'])
raise nbxmpp.NodeProcessed
def _on_bytestream_result(self, con, iq_obj, _properties):
frm = self._ft_get_from(iq_obj)
real_id = iq_obj.getAttr('id')
query = iq_obj.getTag('query')
app.proxy65_manager.resolve_result(frm, query)
try:
streamhost = query.getTag('streamhost-used')
except Exception: # this bytestream result is not what we need
pass
id_ = real_id[3:]
file_props = FilesProp.getFileProp(self._account, id_)
if file_props is None:
raise nbxmpp.NodeProcessed
if streamhost is None:
# proxy approves the activate query
if real_id.startswith('au_'):
if file_props.streamhost_used is False:
raise nbxmpp.NodeProcessed
if not file_props.proxyhosts:
raise nbxmpp.NodeProcessed
for host in file_props.proxyhosts:
if host['initiator'] == frm and \
query.getAttr('sid') == file_props.sid:
app.socks5queue.activate_proxy(host['idx'])
break
raise nbxmpp.NodeProcessed
jid = self._ft_get_streamhost_jid_attr(streamhost)
if file_props.streamhost_used is True:
raise nbxmpp.NodeProcessed
if real_id.startswith('au_'):
if file_props.stopped:
self.remove_transfer(file_props)
else:
app.socks5queue.send_file(file_props, self._account, 'server')
raise nbxmpp.NodeProcessed
proxy = None
if file_props.proxyhosts:
for proxyhost in file_props.proxyhosts:
if proxyhost['jid'] == jid:
proxy = proxyhost
if file_props.stopped:
self.remove_transfer(file_props)
raise nbxmpp.NodeProcessed
if proxy is not None:
file_props.streamhost_used = True
file_props.streamhosts.append(proxy)
file_props.is_a_proxy = True
idx = app.socks5queue.idx
sender = Socks5SenderClient(app.idlequeue,
idx,
app.socks5queue,
_sock=None,
host=str(proxy['host']),
port=int(proxy['port']),
fingerprint=None,
connected=False,
file_props=file_props)
sender.streamhost = proxy
app.socks5queue.add_sockobj(self._account, sender)
proxy['idx'] = sender.queue_idx
app.socks5queue.on_success[file_props.sid] = self._proxy_auth_ok
raise nbxmpp.NodeProcessed
if file_props.stopped:
self.remove_transfer(file_props)
else:
app.socks5queue.send_file(file_props, self._account, 'server')
raise nbxmpp.NodeProcessed
def get_instance(*args, **kwargs):
return Bytestream(*args, **kwargs), 'Bytestream'

View File

@ -0,0 +1,232 @@
# Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0115: Entity Capabilities
import weakref
from collections import defaultdict
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import DiscoIdentity
from nbxmpp.util import compute_caps_hash
from nbxmpp.errors import StanzaError
from gajim.common import app
from gajim.common.const import COMMON_FEATURES
from gajim.common.const import Entity
from gajim.common.helpers import get_optional_features
from gajim.common.nec import NetworkEvent
from gajim.common.task_manager import Task
from gajim.common.modules.base import BaseModule
class Caps(BaseModule):
_nbxmpp_extends = 'EntityCaps'
_nbxmpp_methods = [
'caps',
'set_caps'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._entity_caps,
ns=Namespace.CAPS,
priority=51),
]
self._identities = [
DiscoIdentity(category='client', type='pc', name='Gajim')
]
self._queued_tasks_by_hash = defaultdict(set)
self._queued_tasks_by_jid = {}
def _queue_task(self, task):
old_task = self._get_task(task.entity.jid)
if old_task is not None:
self._remove_task(old_task)
self._log.info('Queue query for hash %s', task.entity.hash)
self._queued_tasks_by_hash[task.entity.hash].add(task)
self._queued_tasks_by_jid[task.entity.jid] = task
app.task_manager.add_task(task)
def _get_task(self, jid):
return self._queued_tasks_by_jid.get(jid)
def _get_similar_tasks(self, task):
return self._queued_tasks_by_hash.pop(task.entity.hash)
def _remove_task(self, task):
task.set_obsolete()
del self._queued_tasks_by_jid[task.entity.jid]
self._queued_tasks_by_hash[task.entity.hash].discard(task)
def _remove_all_tasks(self):
for task in self._queued_tasks_by_jid.values():
task.set_obsolete()
self._queued_tasks_by_jid.clear()
self._queued_tasks_by_hash.clear()
def _entity_caps(self, _con, _stanza, properties):
if properties.type.is_error or properties.type.is_unavailable:
return
if properties.is_self_presence:
return
if properties.entity_caps is None:
return
task = EntityCapsTask(self._account, properties, self._execute_task)
self._log.info('Received %s', task.entity)
disco_info = app.storage.cache.get_caps_entry(task.entity.method,
task.entity.hash)
if disco_info is None:
self._queue_task(task)
return
jid = str(properties.jid)
app.storage.cache.set_last_disco_info(jid, disco_info, cache_only=True)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=jid,
jid=properties.jid.bare))
def _execute_task(self, task):
self._log.info('Request %s from %s', task.entity.hash, task.entity.jid)
self._con.get_module('Discovery').disco_info(
task.entity.jid,
node=f'{task.entity.node}#{task.entity.hash}',
callback=self._on_disco_info,
user_data=task.entity.jid)
def _on_disco_info(self, nbxmpp_task):
jid = nbxmpp_task.get_user_data()
task = self._get_task(jid)
if task is None:
self._log.info('Task not found for %s', jid)
return
self._remove_task(task)
try:
disco_info = nbxmpp_task.finish()
except StanzaError as error:
self._log.warning(error)
return
self._log.info('Disco Info received: %s', disco_info.jid)
try:
compute_caps_hash(disco_info)
except Exception as error:
self._log.warning('Disco info malformed: %s %s',
disco_info.jid, error)
return
app.storage.cache.add_caps_entry(
str(disco_info.jid),
task.entity.method,
disco_info.get_caps_hash(),
disco_info)
self._log.info('Finished query for %s', task.entity.hash)
tasks = self._get_similar_tasks(task)
for task in tasks:
self._remove_task(task)
self._log.info('Update %s', task.entity.jid)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=str(task.entity.jid),
jid=task.entity.jid.bare))
def update_caps(self):
if not app.account_is_connected(self._account):
return
optional_features = get_optional_features(self._account)
self.set_caps(self._identities,
COMMON_FEATURES + optional_features,
'https://gajim.org')
if not app.account_is_available(self._account):
return
app.connections[self._account].change_status(
app.connections[self._account].status,
app.connections[self._account].status_message)
def cleanup(self):
self._remove_all_tasks()
BaseModule.cleanup(self)
class EntityCapsTask(Task):
def __init__(self, account, properties, callback):
Task.__init__(self)
self._account = account
self._callback = weakref.WeakMethod(callback)
self.entity = Entity(jid=properties.jid,
node=properties.entity_caps.node,
hash=properties.entity_caps.ver,
method=properties.entity_caps.hash)
self._from_muc = properties.from_muc
def execute(self):
callback = self._callback()
if callback is not None:
callback(self)
def preconditions_met(self):
try:
client = app.get_client(self._account)
except Exception:
return False
if self._from_muc:
muc = client.get_module('MUC').get_manager().get(
self.entity.jid.bare)
if muc is None or not muc.state.is_joined:
self.set_obsolete()
return False
return client.state.is_available
def __repr__(self):
return f'Entity Caps ({self.entity.jid} {self.entity.hash})'
def __hash__(self):
return hash(self.entity)
def get_instance(*args, **kwargs):
return Caps(*args, **kwargs), 'Caps'

View File

@ -0,0 +1,44 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0280: Message Carbons
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common.modules.base import BaseModule
class Carbons(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.supported = False
def pass_disco(self, info):
if Namespace.CARBONS not in info.features:
return
self.supported = True
self._log.info('Discovered carbons: %s', info.jid)
iq = nbxmpp.Iq('set')
iq.setTag('enable', namespace=Namespace.CARBONS)
self._log.info('Activate')
self._con.connection.send(iq)
def get_instance(*args, **kwargs):
return Carbons(*args, **kwargs), 'Carbons'

View File

@ -0,0 +1,123 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# Chat Markers (XEP-0333)
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.structs import OutgoingMessage
class ChatMarkers(BaseModule):
_nbxmpp_extends = 'ChatMarkers'
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._process_chat_marker,
ns=Namespace.CHATMARKERS,
priority=47),
]
def _process_chat_marker(self, _con, _stanza, properties):
if not properties.is_marker or not properties.marker.is_displayed:
return
if properties.type.is_error:
return
if properties.type.is_groupchat:
manager = self._con.get_module('MUC').get_manager()
muc_data = manager.get(properties.muc_jid)
if muc_data is None:
return
if properties.muc_nickname != muc_data.nick:
return
self._raise_event('read-state-sync', properties)
return
if properties.is_carbon_message and properties.carbon.is_sent:
self._raise_event('read-state-sync', properties)
return
if properties.is_mam_message:
if properties.from_.bareMatch(self._con.get_own_jid()):
return
self._raise_event('displayed-received', properties)
def _raise_event(self, name, properties):
self._log.info('%s: %s %s',
name,
properties.jid,
properties.marker.id)
jid = properties.jid
if not properties.is_muc_pm and not properties.type.is_groupchat:
jid = properties.jid.bare
app.storage.archive.set_marker(
app.get_jid_from_account(self._account),
jid,
properties.marker.id,
'displayed')
app.nec.push_outgoing_event(
NetworkEvent(name,
account=self._account,
jid=jid,
properties=properties,
type=properties.type,
is_muc_pm=properties.is_muc_pm,
marker_id=properties.marker.id))
def _send_marker(self, contact, marker, id_, type_):
jid = contact.jid
if contact.is_pm_contact:
jid = app.get_jid_without_resource(contact.jid)
if type_ in ('gc', 'pm'):
if not app.settings.get_group_chat_setting(
self._account, jid, 'send_marker'):
return
else:
if not app.settings.get_contact_setting(
self._account, jid, 'send_marker'):
return
typ = 'groupchat' if type_ == 'gc' else 'chat'
message = OutgoingMessage(account=self._account,
contact=contact,
message=None,
type_=typ,
marker=(marker, id_),
play_sound=False)
self._con.send_message(message)
self._log.info('Send %s: %s', marker, contact.jid)
def send_displayed_marker(self, contact, id_, type_):
self._send_marker(contact, 'displayed', id_, str(type_))
def get_instance(*args, **kwargs):
return ChatMarkers(*args, **kwargs), 'ChatMarkers'

View File

@ -0,0 +1,358 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0085: Chat State Notifications
from typing import Any
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
from typing import Optional
from typing import Tuple
import time
from functools import wraps
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gi.repository import GLib
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.const import Chatstate as State
from gajim.common.structs import OutgoingMessage
from gajim.common.modules.base import BaseModule
from gajim.common.types import ContactT
from gajim.common.types import ConnectionT
INACTIVE_AFTER = 60
PAUSED_AFTER = 10
def ensure_enabled(func):
@wraps(func)
def func_wrapper(self, *args, **kwargs):
if not self.enabled:
return None
return func(self, *args, **kwargs)
return func_wrapper
class Chatstate(BaseModule):
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._presence_received),
StanzaHandler(name='message',
callback=self._process_chatstate,
ns=Namespace.CHATSTATES,
priority=46),
]
# Our current chatstate with a specific contact
self._chatstates = {} # type: Dict[str, State]
self._last_keyboard_activity = {} # type: Dict[str, float]
self._last_mouse_activity = {} # type: Dict[str, float]
self._timeout_id = None
self._delay_timeout_ids = {} # type: Dict[str, str]
self._blocked = [] # type: List[str]
self._enabled = False
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, value):
if self._enabled == value:
return
self._log.info('Chatstate module %s',
'enabled' if value else 'disabled')
self._enabled = value
if value:
self._timeout_id = GLib.timeout_add_seconds(
2, self._check_last_interaction)
else:
self.cleanup()
self._chatstates = {}
self._last_keyboard_activity = {}
self._last_mouse_activity = {}
self._blocked = []
@ensure_enabled
def _presence_received(self,
_con: ConnectionT,
stanza: nbxmpp.Presence,
_properties: Any) -> None:
if stanza.getType() not in ('unavailable', 'error'):
return
full_jid = stanza.getFrom()
if full_jid is None or self._con.get_own_jid().bare_match(full_jid):
# Presence from ourself
return
contact = app.contacts.get_gc_contact(
self._account, full_jid.bare, full_jid.resource)
if contact is None:
contact = app.contacts.get_contact_from_full_jid(
self._account, str(full_jid))
if contact is None:
return
if contact.chatstate is None:
return
if contact.is_gc_contact:
jid = contact.get_full_jid()
else:
jid = contact.jid
contact.chatstate = None
self._chatstates.pop(jid, None)
self._last_mouse_activity.pop(jid, None)
self._last_keyboard_activity.pop(jid, None)
self._log.info('Reset chatstate for %s', jid)
app.nec.push_outgoing_event(
NetworkEvent('chatstate-received',
account=self._account,
contact=contact))
def _process_chatstate(self, _con, _stanza, properties):
if not properties.has_chatstate:
return
if (properties.is_self_message or
properties.type.is_groupchat or
properties.is_mam_message or
properties.is_carbon_message and properties.carbon.is_sent):
return
if properties.is_muc_pm:
contact = app.contacts.get_gc_contact(
self._account,
properties.jid.bare,
properties.jid.resource)
else:
contact = app.contacts.get_contact_from_full_jid(
self._account, str(properties.jid))
if contact is None:
return
contact.chatstate = properties.chatstate
self._log.info('Recv: %-10s - %s', properties.chatstate, properties.jid)
app.nec.push_outgoing_event(
NetworkEvent('chatstate-received',
account=self._account,
contact=contact))
@ensure_enabled
def _check_last_interaction(self) -> GLib.SOURCE_CONTINUE:
now = time.time()
for jid in list(self._last_mouse_activity.keys()):
time_ = self._last_mouse_activity[jid]
current_state = self._chatstates.get(jid)
if current_state is None:
self._last_mouse_activity.pop(jid, None)
self._last_keyboard_activity.pop(jid, None)
continue
if current_state in (State.GONE, State.INACTIVE):
continue
new_chatstate = None
if now - time_ > INACTIVE_AFTER:
new_chatstate = State.INACTIVE
elif current_state == State.COMPOSING:
key_time = self._last_keyboard_activity[jid]
if now - key_time > PAUSED_AFTER:
new_chatstate = State.PAUSED
if new_chatstate is not None:
if self._chatstates.get(jid) != new_chatstate:
contact = app.contacts.get_contact(self._account, jid)
if contact is None:
room, nick = app.get_room_and_nick_from_fjid(jid)
contact = app.contacts.get_gc_contact(
self._account, room, nick)
if contact is not None:
contact = contact.as_contact()
else:
# Contact not found, maybe we left the group chat
# or the contact was removed from the roster
self._log.info(
'Contact %s not found, reset chatstate', jid)
self._chatstates.pop(jid, None)
self._last_mouse_activity.pop(jid, None)
self._last_keyboard_activity.pop(jid, None)
continue
self.set_chatstate(contact, new_chatstate)
return GLib.SOURCE_CONTINUE
@ensure_enabled
def set_active(self, contact: ContactT) -> None:
if contact.settings.get('send_chatstate') == 'disabled':
return
self._last_mouse_activity[contact.jid] = time.time()
self._chatstates[contact.jid] = State.ACTIVE
def get_active_chatstate(self, contact: ContactT) -> Optional[str]:
# determines if we add 'active' on outgoing messages
if contact.settings.get('send_chatstate') == 'disabled':
return None
if not contact.is_groupchat:
# Dont send chatstates to ourself
if self._con.get_own_jid().bare_match(contact.jid):
return None
if not contact.supports(Namespace.CHATSTATES):
return None
self.set_active(contact)
return 'active'
@ensure_enabled
def block_chatstates(self, contact: ContactT, block: bool) -> None:
# Block sending chatstates to a contact
# Used for example if we cycle through the MUC nick list, which
# produces a lot of buffer 'changed' signals from the input textview.
# This would lead to sending ACTIVE -> COMPOSING -> ACTIVE ...
if block:
self._blocked.append(contact.jid)
else:
self._blocked.remove(contact.jid)
@ensure_enabled
def set_chatstate_delayed(self, contact: ContactT, state: State) -> None:
# Used when we go from Composing -> Active after deleting all text
# from the Textview. We delay the Active state because maybe the
# User starts writing again.
self.remove_delay_timeout(contact)
self._delay_timeout_ids[contact.jid] = GLib.timeout_add_seconds(
2, self.set_chatstate, contact, state)
@ensure_enabled
def set_chatstate(self, contact: ContactT, state: State) -> None:
# Dont send chatstates to ourself
if self._con.get_own_jid().bare_match(contact.jid):
return
if contact.jid in self._blocked:
return
self.remove_delay_timeout(contact)
current_state = self._chatstates.get(contact.jid)
setting = contact.settings.get('send_chatstate')
if setting == 'disabled':
# Send a last 'active' state after user disabled chatstates
if current_state is not None:
self._log.info('Disabled for %s', contact.jid)
self._log.info('Send last state: %-10s - %s',
State.ACTIVE, contact.jid)
self._send_chatstate(contact, str(State.ACTIVE))
self._chatstates.pop(contact.jid, None)
self._last_mouse_activity.pop(contact.jid, None)
self._last_keyboard_activity.pop(contact.jid, None)
return
if not contact.is_groupchat:
# Dont leak presence to contacts
# which are not allowed to see our status
if not contact.is_pm_contact:
if contact and contact.sub in ('to', 'none'):
self._log.info('Contact not subscribed: %s', contact.jid)
return
if contact.show == 'offline':
self._log.info('Contact offline: %s', contact.jid)
return
if not contact.supports(Namespace.CHATSTATES):
self._log.info('Chatstates not supported: %s', contact.jid)
return
if state in (State.ACTIVE, State.COMPOSING):
self._last_mouse_activity[contact.jid] = time.time()
if setting == 'composing_only':
if state in (State.INACTIVE, State.GONE):
state = State.ACTIVE
if current_state == state:
return
self._log.info('Send: %-10s - %s', state, contact.jid)
self._send_chatstate(contact, str(state))
self._chatstates[contact.jid] = state
def _send_chatstate(self, contact, chatstate):
type_ = 'groupchat' if contact.is_groupchat else 'chat'
message = OutgoingMessage(account=self._account,
contact=contact,
message=None,
type_=type_,
chatstate=chatstate,
play_sound=False)
self._con.send_message(message)
@ensure_enabled
def set_mouse_activity(self, contact: ContactT, was_paused: bool) -> None:
if contact.settings.get('send_chatstate') == 'disabled':
return
self._last_mouse_activity[contact.jid] = time.time()
if self._chatstates.get(contact.jid) == State.INACTIVE:
if was_paused:
self.set_chatstate(contact, State.PAUSED)
else:
self.set_chatstate(contact, State.ACTIVE)
@ensure_enabled
def set_keyboard_activity(self, contact: ContactT) -> None:
self._last_keyboard_activity[contact.jid] = time.time()
def remove_delay_timeout(self, contact):
timeout = self._delay_timeout_ids.get(contact.jid)
if timeout is not None:
GLib.source_remove(timeout)
del self._delay_timeout_ids[contact.jid]
def remove_all_delay_timeouts(self):
for timeout in self._delay_timeout_ids.values():
GLib.source_remove(timeout)
self._delay_timeout_ids = {}
def cleanup(self):
self.remove_all_delay_timeouts()
if self._timeout_id is not None:
GLib.source_remove(self._timeout_id)
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Chatstate, str]:
return Chatstate(*args, **kwargs), 'Chatstate'

View File

@ -0,0 +1,56 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0083: Nested Roster Groups
from nbxmpp.errors import is_error
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class Delimiter(BaseModule):
_nbxmpp_extends = 'Delimiter'
_nbxmpp_methods = [
'request_delimiter',
'set_delimiter'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.available = False
self.delimiter = '::'
@as_task
def get_roster_delimiter(self):
_task = yield
delimiter = yield self.request_delimiter()
if is_error(delimiter) or delimiter is None:
result = yield self.set_delimiter(self.delimiter)
if is_error(result):
self._con.connect_machine()
return
delimiter = self.delimiter
self.delimiter = delimiter
self.available = True
self._con.connect_machine()
def get_instance(*args, **kwargs):
return Delimiter(*args, **kwargs), 'Delimiter'

View File

@ -0,0 +1,265 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0030: Service Discovery
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from nbxmpp.errors import is_error
from gajim.common import app
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.nec import NetworkEvent
from gajim.common.modules.util import as_task
from gajim.common.modules.base import BaseModule
class Discovery(BaseModule):
_nbxmpp_extends = 'Discovery'
_nbxmpp_methods = [
'disco_info',
'disco_items',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._answer_disco_info,
typ='get',
ns=Namespace.DISCO_INFO),
StanzaHandler(name='iq',
callback=self._answer_disco_items,
typ='get',
ns=Namespace.DISCO_ITEMS),
]
self._account_info = None
self._server_info = None
@property
def account_info(self):
return self._account_info
@property
def server_info(self):
return self._server_info
def discover_server_items(self):
server = self._con.get_own_jid().domain
self.disco_items(server, callback=self._server_items_received)
def _server_items_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning('Server disco failed')
self._log.error(error)
return
self._log.info('Server items received')
self._log.debug(result)
for item in result.items:
if item.node is not None:
# Only disco components
continue
self.disco_info(item.jid, callback=self._server_items_info_received)
def _server_items_info_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning('Server item disco info failed')
self._log.warning(error)
return
self._log.info('Server item info received: %s', result.jid)
self._parse_transports(result)
try:
self._con.get_module('MUC').pass_disco(result)
self._con.get_module('HTTPUpload').pass_disco(result)
self._con.get_module('Bytestream').pass_disco(result)
except nbxmpp.NodeProcessed:
pass
app.nec.push_incoming_event(
NetworkIncomingEvent('server-disco-received'))
def discover_account_info(self):
own_jid = self._con.get_own_jid().bare
self.disco_info(own_jid, callback=self._account_info_received)
def _account_info_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning('Account disco info failed')
self._log.warning(error)
return
self._log.info('Account info received: %s', result.jid)
self._account_info = result
self._con.get_module('MAM').pass_disco(result)
self._con.get_module('PEP').pass_disco(result)
self._con.get_module('PubSub').pass_disco(result)
self._con.get_module('Bookmarks').pass_disco(result)
self._con.get_module('VCardAvatars').pass_disco(result)
self._con.get_module('Caps').update_caps()
def discover_server_info(self):
# Calling this method starts the connect_maschine()
server = self._con.get_own_jid().domain
self.disco_info(server, callback=self._server_info_received)
def _server_info_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.error('Server disco info failed')
self._log.error(error)
return
self._log.info('Server info received: %s', result.jid)
self._server_info = result
self._con.get_module('SecLabels').pass_disco(result)
self._con.get_module('Blocking').pass_disco(result)
self._con.get_module('VCardTemp').pass_disco(result)
self._con.get_module('Carbons').pass_disco(result)
self._con.get_module('HTTPUpload').pass_disco(result)
self._con.get_module('Register').pass_disco(result)
self._con.connect_machine(restart=True)
def _parse_transports(self, info):
for identity in info.identities:
if identity.category not in ('gateway', 'headline'):
continue
self._log.info('Found transport: %s %s %s',
info.jid, identity.category, identity.type)
jid = str(info.jid)
if jid not in app.transport_type:
app.transport_type[jid] = identity.type
if identity.type in self._con.available_transports:
self._con.available_transports[identity.type].append(jid)
else:
self._con.available_transports[identity.type] = [jid]
def _answer_disco_items(self, _con, stanza, _properties):
from_ = stanza.getFrom()
self._log.info('Answer disco items to %s', from_)
if self._con.get_module('AdHocCommands').command_items_query(stanza):
raise nbxmpp.NodeProcessed
node = stanza.getTagAttr('query', 'node')
if node is None:
result = stanza.buildReply('result')
self._con.connection.send(result)
raise nbxmpp.NodeProcessed
if node == Namespace.COMMANDS:
self._con.get_module('AdHocCommands').command_list_query(stanza)
raise nbxmpp.NodeProcessed
def _answer_disco_info(self, _con, stanza, _properties):
from_ = stanza.getFrom()
self._log.info('Answer disco info %s', from_)
if str(from_).startswith('echo.'):
# Service that echos all stanzas, ignore it
raise nbxmpp.NodeProcessed
if self._con.get_module('AdHocCommands').command_info_query(stanza):
raise nbxmpp.NodeProcessed
@as_task
def disco_muc(self,
jid,
request_vcard=False,
allow_redirect=False):
_task = yield
self._log.info('Request MUC info for %s', jid)
result = yield self._nbxmpp('MUC').request_info(
jid,
request_vcard=request_vcard,
allow_redirect=allow_redirect)
if is_error(result):
raise result
if result.redirected:
self._log.info('MUC info received after redirect: %s -> %s',
jid, result.info.jid)
else:
self._log.info('MUC info received: %s', result.info.jid)
app.storage.cache.set_last_disco_info(result.info.jid, result.info)
if result.vcard is not None:
avatar, avatar_sha = result.vcard.get_avatar()
if avatar is not None:
if not app.interface.avatar_exists(avatar_sha):
app.interface.save_avatar(avatar)
app.storage.cache.set_muc_avatar_sha(result.info.jid,
avatar_sha)
app.interface.avatar_storage.invalidate_cache(result.info.jid)
self._con.get_module('VCardAvatars').muc_disco_info_update(result.info)
app.nec.push_incoming_event(NetworkEvent(
'muc-disco-update',
account=self._account,
room_jid=result.info.jid))
yield result
@as_task
def disco_contact(self, contact):
_task = yield
fjid = contact.get_full_jid()
result = yield self.disco_info(fjid)
if is_error(result):
raise result
self._log.info('Disco Info received: %s', fjid)
app.storage.cache.set_last_disco_info(result.jid,
result,
cache_only=True)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=fjid,
jid=contact.jid))
def get_instance(*args, **kwargs):
return Discovery(*args, **kwargs), 'Discovery'

View File

@ -0,0 +1,116 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0202: Entity Time
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.date_and_time import parse_datetime
from nbxmpp.modules.date_and_time import create_tzinfo
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class EntityTime(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._answer_request,
typ='get',
ns=Namespace.TIME_REVISED),
]
def request_entity_time(self, jid, resource):
if not app.account_is_available(self._account):
return
if resource:
jid += '/' + resource
iq = nbxmpp.Iq(to=jid, typ='get')
iq.addChild('time', namespace=Namespace.TIME_REVISED)
self._log.info('Requested: %s', jid)
self._con.connection.SendAndCallForResponse(iq, self._result_received)
def _result_received(self, _nbxmpp_client, stanza):
time_info = None
if not nbxmpp.isResultNode(stanza):
self._log.info('Error: %s', stanza.getError())
else:
time_info = self._extract_info(stanza)
self._log.info('Received: %s %s', stanza.getFrom(), time_info)
app.nec.push_incoming_event(NetworkEvent('time-result-received',
conn=self._con,
jid=stanza.getFrom(),
time_info=time_info))
def _extract_info(self, stanza):
time_ = stanza.getTag('time')
if not time_:
self._log.warning('No time node: %s', stanza)
return None
tzo = time_.getTag('tzo').getData()
if not tzo:
self._log.warning('Wrong tzo node: %s', stanza)
return None
remote_tz = create_tzinfo(tz_string=tzo)
if remote_tz is None:
self._log.warning('Wrong tzo node: %s', stanza)
return None
utc_time = time_.getTag('utc').getData()
date_time = parse_datetime(utc_time, check_utc=True)
if date_time is None:
self._log.warning('Wrong timezone definition: %s %s',
utc_time, stanza.getFrom())
return None
date_time = date_time.astimezone(remote_tz)
return date_time.strftime('%c %Z')
def _answer_request(self, _con, stanza, _properties):
self._log.info('%s asked for the time', stanza.getFrom())
if app.settings.get_account_setting(self._account, 'send_time_info'):
iq = stanza.buildReply('result')
time_ = iq.setTag('time', namespace=Namespace.TIME_REVISED)
formated_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
time_.setTagData('utc', formated_time)
isdst = time.localtime().tm_isdst
zone = -(time.timezone, time.altzone)[isdst] / 60.0
tzo = (zone / 60, abs(zone % 60))
time_.setTagData('tzo', '%+03d:%02d' % (tzo))
self._log.info('Answer: %s %s', formated_time, '%+03d:%02d' % (tzo))
else:
iq = stanza.buildReply('error')
err = nbxmpp.ErrorNode(nbxmpp.ERR_SERVICE_UNAVAILABLE)
iq.addChild(node=err)
self._log.info('Send service-unavailable')
self._con.connection.send(iq)
raise nbxmpp.NodeProcessed
def get_instance(*args, **kwargs):
return EntityTime(*args, **kwargs), 'EntityTime'

View File

@ -0,0 +1,101 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0100: Gateway Interaction
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class Gateway(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
def unsubscribe(self, agent):
if not app.account_is_available(self._account):
return
iq = nbxmpp.Iq('set', Namespace.REGISTER, to=agent)
iq.setQuery().setTag('remove')
self._con.connection.SendAndCallForResponse(
iq, self._on_unsubscribe_result)
self._con.get_module('Roster').del_item(agent)
def _on_unsubscribe_result(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Error: %s', stanza.getError())
return
agent = stanza.getFrom().bare
jid_list = []
for jid in app.contacts.get_jid_list(self._account):
if jid.endswith('@' + agent):
jid_list.append(jid)
self._log.info('Removing contact %s due to'
' unregistered transport %s', jid, agent)
self._con.get_module('Presence').unsubscribe(jid)
# Transport contacts can't have 2 resources
if jid in app.to_be_removed[self._account]:
# This way we'll really remove it
app.to_be_removed[self._account].remove(jid)
app.nec.push_incoming_event(
NetworkEvent('agent-removed',
conn=self._con,
agent=agent,
jid_list=jid_list))
def request_gateway_prompt(self, jid, prompt=None):
typ_ = 'get'
if prompt:
typ_ = 'set'
iq = nbxmpp.Iq(typ=typ_, to=jid)
query = iq.addChild(name='query', namespace=Namespace.GATEWAY)
if prompt:
query.setTagData('prompt', prompt)
self._con.connection.SendAndCallForResponse(iq, self._on_prompt_result)
def _on_prompt_result(self, _nbxmpp_client, stanza):
jid = str(stanza.getFrom())
fjid = stanza.getFrom().bare
resource = stanza.getFrom().resource
query = stanza.getTag('query')
if query is not None:
desc = query.getTagData('desc')
prompt = query.getTagData('prompt')
prompt_jid = query.getTagData('jid')
else:
desc = None
prompt = None
prompt_jid = None
app.nec.push_incoming_event(
NetworkEvent('gateway-prompt-received',
conn=self._con,
fjid=fjid,
jid=jid,
resource=resource,
desc=desc,
prompt=prompt,
prompt_jid=prompt_jid,
stanza=stanza))
def get_instance(*args, **kwargs):
return Gateway(*args, **kwargs), 'Gateway'

View File

@ -0,0 +1,78 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0070: Verifying HTTP Requests via XMPP
import nbxmpp
from nbxmpp.structs import StanzaHandler
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class HTTPAuth(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._http_auth,
ns=Namespace.HTTP_AUTH,
priority=45),
StanzaHandler(name='iq',
callback=self._http_auth,
typ='get',
ns=Namespace.HTTP_AUTH,
priority=45)
]
def _http_auth(self, _con, stanza, properties):
if not properties.is_http_auth:
return
self._log.info('Auth request received')
auto_answer = app.settings.get_account_setting(self._account,
'http_auth')
if auto_answer in ('yes', 'no'):
self.build_http_auth_answer(stanza, auto_answer)
raise nbxmpp.NodeProcessed
app.nec.push_incoming_event(
NetworkEvent('http-auth-received',
conn=self._con,
iq_id=properties.http_auth.id,
method=properties.http_auth.method,
url=properties.http_auth.url,
msg=properties.http_auth.body,
stanza=stanza))
raise nbxmpp.NodeProcessed
def build_http_auth_answer(self, stanza, answer):
if answer == 'yes':
self._log.info('Auth request approved')
confirm = stanza.getTag('confirm')
reply = stanza.buildReply('result')
if stanza.getName() == 'message':
reply.addChild(node=confirm)
self._con.connection.send(reply)
elif answer == 'no':
self._log.info('Auth request denied')
err = nbxmpp.Error(stanza, nbxmpp.protocol.ERR_NOT_AUTHORIZED)
self._con.connection.send(err)
def get_instance(*args, **kwargs):
return HTTPAuth(*args, **kwargs), 'HTTPAuth'

View File

@ -0,0 +1,404 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0363: HTTP File Upload
import os
import io
from urllib.parse import urlparse
import mimetypes
from nbxmpp.namespaces import Namespace
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import HTTPUploadStanzaError
from nbxmpp.util import convert_tls_error_flags
from gi.repository import GLib
from gi.repository import Soup
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.helpers import get_tls_error_phrase
from gajim.common.helpers import get_user_proxy
from gajim.common.const import FTState
from gajim.common.filetransfer import FileTransfer
from gajim.common.modules.base import BaseModule
from gajim.common.exceptions import FileError
class HTTPUpload(BaseModule):
_nbxmpp_extends = 'HTTPUpload'
def __init__(self, con):
BaseModule.__init__(self, con)
self.available = False
self.component = None
self.httpupload_namespace = None
self.max_file_size = None # maximum file size in bytes
self._proxy_resolver = None
self._queued_messages = {}
self._session = Soup.Session()
self._session.props.ssl_strict = False
self._session.props.user_agent = 'Gajim %s' % app.version
def _set_proxy_if_available(self):
proxy = get_user_proxy(self._account)
if proxy is None:
self._proxy_resolver = None
self._session.props.proxy_resolver = None
else:
self._proxy_resolver = proxy.get_resolver()
self._session.props.proxy_resolver = self._proxy_resolver
def pass_disco(self, info):
if not info.has_httpupload:
return
self.available = True
self.httpupload_namespace = Namespace.HTTPUPLOAD_0
self.component = info.jid
self.max_file_size = info.httpupload_max_file_size
self._log.info('Discovered component: %s', info.jid)
if self.max_file_size is None:
self._log.warning('Component does not provide maximum file size')
else:
size = GLib.format_size_full(self.max_file_size,
GLib.FormatSizeFlags.IEC_UNITS)
self._log.info('Component has a maximum file size of: %s', size)
for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
ctrl.update_actions()
def make_transfer(self, path, encryption, contact, groupchat=False):
if not path or not os.path.exists(path):
raise FileError(_('Could not access file'))
invalid_file = False
stat = os.stat(path)
if os.path.isfile(path):
if stat[6] == 0:
invalid_file = True
msg = _('File is empty')
else:
invalid_file = True
msg = _('File does not exist')
if self.max_file_size is not None and \
stat.st_size > self.max_file_size:
invalid_file = True
size = GLib.format_size_full(self.max_file_size,
GLib.FormatSizeFlags.IEC_UNITS)
msg = _('File is too large, '
'maximum allowed file size is: %s') % size
if invalid_file:
raise FileError(msg)
mime = mimetypes.MimeTypes().guess_type(path)[0]
if not mime:
mime = 'application/octet-stream' # fallback mime type
self._log.info("Detected MIME type of file: %s", mime)
return HTTPFileTransfer(self._account,
path,
contact,
mime,
encryption,
groupchat)
def cancel_transfer(self, transfer):
transfer.set_cancelled()
message = self._queued_messages.get(id(transfer))
if message is None:
return
self._session.cancel_message(message, Soup.Status.CANCELLED)
def start_transfer(self, transfer):
if transfer.encryption is not None and not transfer.is_encrypted:
transfer.set_encrypting()
plugin = app.plugin_manager.encryption_plugins[transfer.encryption]
if hasattr(plugin, 'encrypt_file'):
plugin.encrypt_file(transfer,
self._account,
self.start_transfer)
else:
transfer.set_error('encryption-not-available')
return
transfer.set_preparing()
self._log.info('Sending request for slot')
self._nbxmpp('HTTPUpload').request_slot(
jid=self.component,
filename=transfer.filename,
size=transfer.size,
content_type=transfer.mime,
callback=self._received_slot,
user_data=transfer)
def _received_slot(self, task):
transfer = task.get_user_data()
try:
result = task.finish()
except (StanzaError,
HTTPUploadStanzaError,
MalformedStanzaError) as error:
if error.app_condition == 'file-too-large':
size_text = GLib.format_size_full(
error.get_max_file_size(),
GLib.FormatSizeFlags.IEC_UNITS)
error_text = _('File is too large, '
'maximum allowed file size is: %s' % size_text)
transfer.set_error('file-too-large', error_text)
else:
transfer.set_error('misc', str(error))
return
transfer.process_result(result)
if (urlparse(transfer.put_uri).scheme != 'https' or
urlparse(transfer.get_uri).scheme != 'https'):
transfer.set_error('unsecure')
return
self._log.info('Uploading file to %s', transfer.put_uri)
self._log.info('Please download from %s', transfer.get_uri)
self._upload_file(transfer)
def _upload_file(self, transfer):
transfer.set_started()
message = Soup.Message.new('PUT', transfer.put_uri)
message.connect('starting', self._check_certificate, transfer)
# Set CAN_REBUILD so chunks get discarded after they have been
# written to the network
message.set_flags(Soup.MessageFlags.CAN_REBUILD |
Soup.MessageFlags.NO_REDIRECT)
message.props.request_body.set_accumulate(False)
message.props.request_headers.set_content_type(transfer.mime, None)
message.props.request_headers.set_content_length(transfer.size)
for name, value in transfer.headers.items():
message.props.request_headers.append(name, value)
message.connect('wrote-headers', self._on_wrote_headers, transfer)
message.connect('wrote-chunk', self._on_wrote_chunk, transfer)
self._queued_messages[id(transfer)] = message
self._set_proxy_if_available()
self._session.queue_message(message, self._on_finish, transfer)
def _check_certificate(self, message, transfer):
https_used, tls_certificate, tls_errors = message.get_https_status()
if not https_used:
self._log.warning('HTTPS was not used for upload')
transfer.set_error('unsecure')
self._session.cancel_message(message, Soup.Status.CANCELLED)
return
tls_errors = convert_tls_error_flags(tls_errors)
if app.cert_store.verify(tls_certificate, tls_errors):
return
for error in tls_errors:
phrase = get_tls_error_phrase(error)
self._log.warning('TLS verification failed: %s', phrase)
transfer.set_error('tls-verification-failed', phrase)
self._session.cancel_message(message, Soup.Status.CANCELLED)
def _on_finish(self, _session, message, transfer):
self._queued_messages.pop(id(transfer), None)
if message.props.status_code == Soup.Status.CANCELLED:
self._log.info('Upload cancelled')
return
if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
self._log.info('Upload completed successfully')
transfer.set_finished()
else:
phrase = Soup.Status.get_phrase(message.props.status_code)
self._log.error('Got unexpected http upload response code: %s',
phrase)
transfer.set_error('http-response', phrase)
def _on_wrote_chunk(self, message, transfer):
transfer.update_progress()
if transfer.is_complete:
message.props.request_body.complete()
return
bytes_ = transfer.get_chunk()
self._session.pause_message(message)
GLib.idle_add(self._append, message, bytes_)
def _append(self, message, bytes_):
if message.props.status_code == Soup.Status.CANCELLED:
return
self._session.unpause_message(message)
message.props.request_body.append(bytes_)
@staticmethod
def _on_wrote_headers(message, transfer):
message.props.request_body.append(transfer.get_chunk())
class HTTPFileTransfer(FileTransfer):
_state_descriptions = {
FTState.ENCRYPTING: _('Encrypting file…'),
FTState.PREPARING: _('Requesting HTTP File Upload Slot…'),
FTState.STARTED: _('Uploading via HTTP File Upload…'),
}
_errors = {
'unsecure': _('The server returned an insecure transport (HTTP).'),
'encryption-not-available': _('There is no encryption method available '
'for the chosen encryption.')
}
def __init__(self,
account,
path,
contact,
mime,
encryption,
groupchat):
FileTransfer.__init__(self, account)
self._path = path
self._encryption = encryption
self._groupchat = groupchat
self._contact = contact
self._mime = mime
self.size = os.stat(path).st_size
self.put_uri = None
self.get_uri = None
self._uri_transform_func = None
self._stream = None
self._data = None
self._headers = {}
self._is_encrypted = False
@property
def mime(self):
return self._mime
@property
def contact(self):
return self._contact
@property
def is_groupchat(self):
return self._groupchat
@property
def encryption(self):
return self._encryption
@property
def headers(self):
return self._headers
@property
def path(self):
return self._path
@property
def is_encrypted(self):
return self._is_encrypted
def get_transformed_uri(self):
if self._uri_transform_func is not None:
return self._uri_transform_func(self.get_uri)
return self.get_uri
def set_uri_transform_func(self, func):
self._uri_transform_func = func
@property
def filename(self):
return os.path.basename(self._path)
def set_error(self, domain, text=''):
if not text:
text = self._errors[domain]
self._close()
super().set_error(domain, text)
def set_finished(self):
self._close()
super().set_finished()
def set_encrypted_data(self, data):
self._data = data
self._is_encrypted = True
def _close(self):
if self._stream is not None:
self._stream.close()
def get_chunk(self):
if self._stream is None:
if self._encryption is None:
self._stream = open(self._path, 'rb')
else:
self._stream = io.BytesIO(self._data)
data = self._stream.read(16384)
if not data:
self._close()
return None
self._seen += len(data)
if self.is_complete:
self._close()
return data
def get_data(self):
with open(self._path, 'rb') as file:
data = file.read()
return data
def process_result(self, result):
self.put_uri = result.put_uri
self.get_uri = result.get_uri
self._headers = result.headers
def get_instance(*args, **kwargs):
return HTTPUpload(*args, **kwargs), 'HTTPUpload'

239
gajim/common/modules/ibb.py Normal file
View File

@ -0,0 +1,239 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0047: In-Band Bytestreams
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from gajim.common import app
from gajim.common.helpers import to_user_string
from gajim.common.modules.base import BaseModule
from gajim.common.file_props import FilesProp
class IBB(BaseModule):
_nbxmpp_extends = 'IBB'
_nbxmpp_methods = [
'send_open',
'send_close',
'send_data',
'send_reply',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._ibb_received,
ns=Namespace.IBB),
]
def _ibb_received(self, _con, stanza, properties):
if not properties.is_ibb:
return
if properties.ibb.type == 'data':
self._log.info('Data received, sid: %s, seq: %s',
properties.ibb.sid, properties.ibb.seq)
file_props = FilesProp.getFilePropByTransportSid(self._account,
properties.ibb.sid)
if not file_props:
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
raise NodeProcessed
if file_props.connected:
self._on_data_received(stanza, file_props, properties)
self.send_reply(stanza)
elif properties.ibb.type == 'open':
self._log.info('Open received, sid: %s, blocksize: %s',
properties.ibb.sid, properties.ibb.block_size)
file_props = FilesProp.getFilePropByTransportSid(self._account,
properties.ibb.sid)
if not file_props:
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
raise NodeProcessed
file_props.block_size = properties.ibb.block_size
file_props.direction = '<'
file_props.seq = 0
file_props.received_len = 0
file_props.last_time = time.time()
file_props.error = 0
file_props.paused = False
file_props.connected = True
file_props.completed = False
file_props.disconnect_cb = None
file_props.continue_cb = None
file_props.syn_id = stanza.getID()
file_props.fp = open(file_props.file_name, 'wb')
self.send_reply(stanza)
elif properties.ibb.type == 'close':
self._log.info('Close received, sid: %s', properties.ibb.sid)
file_props = FilesProp.getFilePropByTransportSid(self._account,
properties.ibb.sid)
if not file_props:
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
raise NodeProcessed
self.send_reply(stanza)
file_props.fp.close()
file_props.completed = file_props.received_len >= file_props.size
if not file_props.completed:
file_props.error = -1
app.socks5queue.complete_transfer_cb(self._account, file_props)
raise NodeProcessed
def _on_data_received(self, stanza, file_props, properties):
ibb = properties.ibb
if ibb.seq != file_props.seq:
self.send_reply(stanza, nbxmpp.ERR_UNEXPECTED_REQUEST)
self.send_close(file_props)
raise NodeProcessed
self._log.debug('Data received: sid: %s, %s+%s bytes',
ibb.sid, file_props.fp.tell(), len(ibb.data))
file_props.seq += 1
file_props.started = True
file_props.fp.write(ibb.data)
current_time = time.time()
file_props.elapsed_time += current_time - file_props.last_time
file_props.last_time = current_time
file_props.received_len += len(ibb.data)
app.socks5queue.progress_transfer_cb(self._account, file_props)
if file_props.received_len >= file_props.size:
file_props.completed = True
def send_open(self, to, sid, fp):
self._log.info('Send open to %s, sid: %s', to, sid)
file_props = FilesProp.getFilePropBySid(sid)
file_props.direction = '>'
file_props.block_size = 4096
file_props.fp = fp
file_props.seq = -1
file_props.error = 0
file_props.paused = False
file_props.received_len = 0
file_props.last_time = time.time()
file_props.connected = True
file_props.completed = False
file_props.disconnect_cb = None
file_props.continue_cb = None
self._nbxmpp('IBB').send_open(to,
file_props.transport_sid,
4096,
callback=self._on_open_result,
user_data=file_props)
return file_props
def _on_open_result(self, task):
try:
task.finish()
except StanzaError as error:
app.socks5queue.error_cb('Error', to_user_string(error))
self._log.warning(error)
return
file_props = task.get_user_data()
self.send_data(file_props)
def send_close(self, file_props):
file_props.connected = False
file_props.fp.close()
file_props.stopped = True
to = file_props.receiver
if file_props.direction == '<':
to = file_props.sender
self._log.info('Send close to %s, sid: %s',
to, file_props.transport_sid)
self._nbxmpp('IBB').send_close(to, file_props.transport_sid,
callback=self._on_close_result)
if file_props.completed:
app.socks5queue.complete_transfer_cb(self._account, file_props)
else:
if file_props.type_ == 's':
peerjid = file_props.receiver
else:
peerjid = file_props.sender
session = self._con.get_module('Jingle').get_jingle_session(
peerjid, file_props.sid, 'file')
# According to the xep, the initiator also cancels
# the jingle session if there are no more files to send using IBB
if session.weinitiate:
session.cancel_session()
def _on_close_result(self, task):
try:
task.finish()
except StanzaError as error:
app.socks5queue.error_cb('Error', to_user_string(error))
self._log.warning(error)
return
def send_data(self, file_props):
if file_props.completed:
self.send_close(file_props)
return
chunk = file_props.fp.read(file_props.block_size)
if chunk:
file_props.seq += 1
file_props.started = True
if file_props.seq == 65536:
file_props.seq = 0
self._log.info('Send data to %s, sid: %s',
file_props.receiver, file_props.transport_sid)
self._nbxmpp('IBB').send_data(file_props.receiver,
file_props.transport_sid,
file_props.seq,
chunk,
callback=self._on_data_result,
user_data=file_props)
current_time = time.time()
file_props.elapsed_time += current_time - file_props.last_time
file_props.last_time = current_time
file_props.received_len += len(chunk)
if file_props.size == file_props.received_len:
file_props.completed = True
app.socks5queue.progress_transfer_cb(self._account, file_props)
def _on_data_result(self, task):
try:
task.finish()
except StanzaError as error:
app.socks5queue.error_cb('Error', to_user_string(error))
self._log.warning(error)
return
file_props = task.get_user_data()
self.send_data(file_props)
def get_instance(*args, **kwargs):
return IBB(*args, **kwargs), 'IBB'

View File

@ -0,0 +1,88 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# Iq handler
import nbxmpp
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.helpers import to_user_string
from gajim.common.nec import NetworkEvent
from gajim.common.file_props import FilesProp
from gajim.common.modules.base import BaseModule
class Iq(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._iq_error_received,
typ='error',
priority=51),
]
def _iq_error_received(self, _con, _stanza, properties):
self._log.info('Error: %s', properties.error)
if properties.error.condition in ('jid-malformed',
'forbidden',
'not-acceptable'):
sid = self._get_sid(properties.id)
file_props = FilesProp.getFileProp(self._account, sid)
if file_props:
if properties.error.condition == 'jid-malformed':
file_props.error = -3
else:
file_props.error = -4
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=properties.jid.bare,
file_props=file_props,
error_msg=to_user_string(properties.error)))
self._con.get_module('Bytestream').disconnect_transfer(
file_props)
raise nbxmpp.NodeProcessed
if properties.error.condition == 'item-not-found':
sid = self._get_sid(properties.id)
file_props = FilesProp.getFileProp(self._account, sid)
if file_props:
app.nec.push_incoming_event(
NetworkEvent('file-send-error',
account=self._account,
jid=str(properties.jid),
file_props=file_props))
self._con.get_module('Bytestream').disconnect_transfer(
file_props)
raise nbxmpp.NodeProcessed
app.nec.push_incoming_event(
NetworkEvent('iq-error-received',
account=self._account,
properties=properties))
raise nbxmpp.NodeProcessed
@staticmethod
def _get_sid(id_):
sid = id_
if len(id_) > 3 and id_[2] == '_':
sid = id_[3:]
return sid
def get_instance(*args, **kwargs):
return Iq(*args, **kwargs), 'Iq'

View File

@ -0,0 +1,311 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
"""
Handles the jingle signalling protocol
"""
#TODO:
# * things in XEP 0176, including:
# - http://xmpp.org/extensions/xep-0176.html#protocol-restarts
# - http://xmpp.org/extensions/xep-0176.html#fallback
# * XEP 0177 (raw udp)
# * UI:
# - make state and codec information available to the user
# - video integration
# * config:
# - codecs
import logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import helpers
from gajim.common import app
from gajim.common import jingle_xtls
from gajim.common.modules.base import BaseModule
from gajim.common.jingle_session import JingleSession
from gajim.common.jingle_session import JingleStates
from gajim.common.jingle_ft import JingleFileTransfer
from gajim.common.jingle_transport import JingleTransportSocks5
from gajim.common.jingle_transport import JingleTransportIBB
from gajim.common.jingle_rtp import JingleAudio, JingleVideo
logger = logging.getLogger('gajim.c.m.jingle')
class Jingle(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
typ='result',
callback=self._on_jingle_iq),
StanzaHandler(name='iq',
typ='error',
callback=self._on_jingle_iq),
StanzaHandler(name='iq',
typ='set',
ns=Namespace.JINGLE,
callback=self._on_jingle_iq),
StanzaHandler(name='iq',
typ='get',
ns=Namespace.PUBKEY_PUBKEY,
callback=self._on_pubkey_request),
StanzaHandler(name='iq',
typ='result',
ns=Namespace.PUBKEY_PUBKEY,
callback=self._pubkey_result_received),
]
# dictionary: sessionid => JingleSession object
self._sessions = {}
# dictionary: (jid, iq stanza id) => JingleSession object,
# one time callbacks
self.__iq_responses = {}
self.files = []
def delete_jingle_session(self, sid):
"""
Remove a jingle session from a jingle stanza dispatcher
"""
if sid in self._sessions:
#FIXME: Move this elsewhere?
for content in list(self._sessions[sid].contents.values()):
content.destroy()
self._sessions[sid].callbacks = []
del self._sessions[sid]
def _on_pubkey_request(self, con, stanza, _properties):
jid_from = helpers.get_full_jid_from_iq(stanza)
self._log.info('Pubkey request from %s', jid_from)
sid = stanza.getAttr('id')
jingle_xtls.send_cert(con, jid_from, sid)
raise nbxmpp.NodeProcessed
def _pubkey_result_received(self, con, stanza, _properties):
jid_from = helpers.get_full_jid_from_iq(stanza)
self._log.info('Pubkey result from %s', jid_from)
jingle_xtls.handle_new_cert(con, stanza, jid_from)
def _on_jingle_iq(self, _con, stanza, _properties):
"""
The jingle stanza dispatcher
Route jingle stanza to proper JingleSession object, or create one if it
is a new session.
TODO: Also check if the stanza isn't an error stanza, if so route it
adequately.
"""
# get data
try:
jid = helpers.get_full_jid_from_iq(stanza)
except helpers.InvalidFormat:
logger.warning('Invalid JID: %s, ignoring it', stanza.getFrom())
return
id_ = stanza.getID()
if (jid, id_) in self.__iq_responses.keys():
self.__iq_responses[(jid, id_)].on_stanza(stanza)
del self.__iq_responses[(jid, id_)]
raise nbxmpp.NodeProcessed
jingle = stanza.getTag('jingle')
# a jingle element is not necessary in iq-result stanza
# don't check for that
if jingle:
sid = jingle.getAttr('sid')
else:
sid = None
for sesn in self._sessions.values():
if id_ in sesn.iq_ids:
sesn.on_stanza(stanza)
return
# do we need to create a new jingle object
if sid not in self._sessions:
#TODO: tie-breaking and other things...
newjingle = JingleSession(self._con, weinitiate=False, jid=jid,
iq_id=id_, sid=sid)
self._sessions[sid] = newjingle
# we already have such session in dispatcher...
self._sessions[sid].collect_iq_id(id_)
self._sessions[sid].on_stanza(stanza)
# Delete invalid/unneeded sessions
if sid in self._sessions and \
self._sessions[sid].state == JingleStates.ENDED:
self.delete_jingle_session(sid)
raise nbxmpp.NodeProcessed
def start_audio(self, jid):
if self.get_jingle_session(jid, media='audio'):
return self.get_jingle_session(jid, media='audio').sid
jingle = self.get_jingle_session(jid, media='video')
if jingle:
jingle.add_content('voice', JingleAudio(jingle))
else:
jingle = JingleSession(self._con, weinitiate=True, jid=jid)
self._sessions[jingle.sid] = jingle
jingle.add_content('voice', JingleAudio(jingle))
jingle.start_session()
return jingle.sid
def start_video(self, jid):
if self.get_jingle_session(jid, media='video'):
return self.get_jingle_session(jid, media='video').sid
jingle = self.get_jingle_session(jid, media='audio')
if jingle:
video = JingleVideo(jingle)
jingle.add_content('video', video)
else:
jingle = JingleSession(self._con, weinitiate=True, jid=jid)
self._sessions[jingle.sid] = jingle
video = JingleVideo(jingle)
jingle.add_content('video', video)
jingle.start_session()
return jingle.sid
def start_audio_video(self, jid):
if self.get_jingle_session(jid, media='video'):
return self.get_jingle_session(jid, media='video').sid
audio_session = self.get_jingle_session(jid, media='audio')
video_session = self.get_jingle_session(jid, media='video')
if audio_session and video_session:
return audio_session.sid
if audio_session:
video = JingleVideo(audio_session)
audio_session.add_content('video', video)
return audio_session.sid
if video_session:
audio = JingleAudio(video_session)
video_session.add_content('audio', audio)
return video_session.sid
jingle_session = JingleSession(self._con, weinitiate=True, jid=jid)
self._sessions[jingle_session.sid] = jingle_session
audio = JingleAudio(jingle_session)
video = JingleVideo(jingle_session)
jingle_session.add_content('audio', audio)
jingle_session.add_content('video', video)
jingle_session.start_session()
return jingle_session.sid
def start_file_transfer(self, jid, file_props, request=False):
logger.info("start file transfer with file: %s", file_props)
contact = app.contacts.get_contact_with_highest_priority(
self._account, app.get_jid_without_resource(jid))
if app.contacts.is_gc_contact(self._account, jid):
gcc = jid.split('/')
if len(gcc) == 2:
contact = app.contacts.get_gc_contact(self._account,
gcc[0],
gcc[1])
if contact is None:
return None
use_security = contact.supports(Namespace.JINGLE_XTLS)
jingle = JingleSession(self._con,
weinitiate=True,
jid=jid,
werequest=request)
# this is a file transfer
jingle.session_type_ft = True
self._sessions[jingle.sid] = jingle
file_props.sid = jingle.sid
if contact.supports(Namespace.JINGLE_BYTESTREAM):
transport = JingleTransportSocks5()
elif contact.supports(Namespace.JINGLE_IBB):
transport = JingleTransportIBB()
else:
transport = None
senders = 'initiator'
if request:
senders = 'responder'
transfer = JingleFileTransfer(jingle,
transport=transport,
file_props=file_props,
use_security=use_security,
senders=senders)
file_props.transport_sid = transport.sid
file_props.algo = self.__hash_support(contact)
jingle.add_content('file' + helpers.get_random_string(), transfer)
jingle.start_session()
return transfer.transport.sid
@staticmethod
def __hash_support(contact):
if contact.supports(Namespace.HASHES_2):
if contact.supports(Namespace.HASHES_BLAKE2B_512):
return 'blake2b-512'
if contact.supports(Namespace.HASHES_BLAKE2B_256):
return 'blake2b-256'
if contact.supports(Namespace.HASHES_SHA3_512):
return 'sha3-512'
if contact.supports(Namespace.HASHES_SHA3_256):
return 'sha3-256'
if contact.supports(Namespace.HASHES_SHA512):
return 'sha-512'
if contact.supports(Namespace.HASHES_SHA256):
return 'sha-256'
return None
def get_jingle_sessions(self, jid, sid=None, media=None):
if sid:
return [se for se in self._sessions.values() if se.sid == sid]
sessions = [se for se in self._sessions.values() if se.peerjid == jid]
if media:
if media not in ('audio', 'video', 'file'):
return []
return [se for se in sessions if se.get_content(media)]
return sessions
def set_file_info(self, file_):
# Saves information about the files we have transferred
# in case they need to be requested again.
self.files.append(file_)
def get_file_info(self, peerjid, hash_=None, name=None, _account=None):
if hash_:
for file in self.files: # DEBUG
#if f['hash'] == '1294809248109223':
if file['hash'] == hash_ and file['peerjid'] == peerjid:
return file
elif name:
for file in self.files:
if file['name'] == name and file['peerjid'] == peerjid:
return file
return None
def get_jingle_session(self, jid, sid=None, media=None):
if sid:
if sid in self._sessions:
return self._sessions[sid]
return None
if media:
if media not in ('audio', 'video', 'file'):
return None
for session in self._sessions.values():
if session.peerjid == jid and session.get_content(media):
return session
return None
def get_instance(*args, **kwargs):
return Jingle(*args, **kwargs), 'Jingle'

View File

@ -0,0 +1,59 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0012: Last Activity
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common import idle
from gajim.common.modules.base import BaseModule
class LastActivity(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
typ='get',
callback=self._answer_request,
ns=Namespace.LAST),
]
def _answer_request(self, _con, stanza, properties):
self._log.info('Request from %s', properties.jid)
allow_send = app.settings.get_account_setting(self._account,
'send_idle_time')
if app.is_installed('IDLE') and allow_send:
iq = stanza.buildReply('result')
query = iq.setQuery()
seconds = idle.Monitor.get_idle_sec()
query.attrs['seconds'] = seconds
self._log.info('Respond with seconds: %s', seconds)
else:
iq = stanza.buildReply('error')
err = nbxmpp.ErrorNode(nbxmpp.ERR_SERVICE_UNAVAILABLE)
iq.addChild(node=err)
self._con.connection.send(iq)
raise nbxmpp.NodeProcessed
def get_instance(*args, **kwargs):
return LastActivity(*args, **kwargs), 'LastActivity'

507
gajim/common/modules/mam.py Normal file
View File

@ -0,0 +1,507 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0313: Message Archive Management
import time
from datetime import datetime
from datetime import timedelta
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import is_error
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.util import raise_if_error
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.const import ArchiveState
from gajim.common.const import KindConstant
from gajim.common.const import SyncThreshold
from gajim.common.helpers import AdditionalDataDict
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.util import get_eme_message
from gajim.common.modules.util import as_task
from gajim.common.modules.base import BaseModule
class MAM(BaseModule):
_nbxmpp_extends = 'MAM'
_nbxmpp_methods = [
'request_preferences',
'set_preferences',
'make_query',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._set_message_archive_info,
priority=41),
StanzaHandler(name='message',
callback=self._mam_message_received,
priority=51),
]
self.available = False
self._mam_query_ids = {}
# Holds archive jids where catch up was successful
self._catch_up_finished = []
def pass_disco(self, info):
if Namespace.MAM_2 not in info.features:
return
self.available = True
self._log.info('Discovered MAM: %s', info.jid)
app.nec.push_incoming_event(
NetworkEvent('feature-discovered',
account=self._account,
feature=Namespace.MAM_2))
def reset_state(self):
self._mam_query_ids.clear()
self._catch_up_finished.clear()
def _remove_query_id(self, jid):
self._mam_query_ids.pop(jid, None)
def is_catch_up_finished(self, jid):
return jid in self._catch_up_finished
def _from_valid_archive(self, _stanza, properties):
if properties.type.is_groupchat:
expected_archive = properties.jid
else:
expected_archive = self._con.get_own_jid()
return properties.mam.archive.bare_match(expected_archive)
def _get_unique_id(self, properties):
if properties.type.is_groupchat:
return properties.mam.id, None
if properties.is_self_message:
return None, properties.id
if properties.is_muc_pm:
return properties.mam.id, properties.id
if self._con.get_own_jid().bare_match(properties.from_):
# message we sent
return properties.mam.id, properties.id
# A message we received
return properties.mam.id, None
def _set_message_archive_info(self, _con, _stanza, properties):
if (properties.is_mam_message or
properties.is_pubsub or
properties.is_muc_subject):
return
if properties.type.is_groupchat:
archive_jid = properties.jid.bare
timestamp = properties.timestamp
disco_info = app.storage.cache.get_last_disco_info(archive_jid)
if disco_info is None:
# This is the case on MUC creation
# After MUC configuration we receive a configuration change
# message before we had the chance to disco the new MUC
return
if disco_info.mam_namespace != Namespace.MAM_2:
return
else:
if not self.available:
return
archive_jid = self._con.get_own_jid().bare
timestamp = None
if properties.stanza_id is None:
return
if not archive_jid == properties.stanza_id.by:
return
if not self.is_catch_up_finished(archive_jid):
return
app.storage.archive.set_archive_infos(
archive_jid,
last_mam_id=properties.stanza_id.id,
last_muc_timestamp=timestamp)
def _mam_message_received(self, _con, stanza, properties):
if not properties.is_mam_message:
return
app.nec.push_incoming_event(
NetworkIncomingEvent('mam-message-received',
account=self._account,
stanza=stanza,
properties=properties))
if not self._from_valid_archive(stanza, properties):
self._log.warning('Message from invalid archive %s',
properties.mam.archive)
raise nbxmpp.NodeProcessed
self._log.info('Received message from archive: %s',
properties.mam.archive)
if not self._is_valid_request(properties):
self._log.warning('Invalid MAM Message: unknown query id %s',
properties.mam.query_id)
self._log.debug(stanza)
raise nbxmpp.NodeProcessed
is_groupchat = properties.type.is_groupchat
if is_groupchat:
kind = KindConstant.GC_MSG
else:
if properties.from_.bare_match(self._con.get_own_jid()):
kind = KindConstant.CHAT_MSG_SENT
else:
kind = KindConstant.CHAT_MSG_RECV
stanza_id, message_id = self._get_unique_id(properties)
# Search for duplicates
if app.storage.archive.find_stanza_id(self._account,
str(properties.mam.archive),
stanza_id,
message_id,
groupchat=is_groupchat):
self._log.info('Found duplicate with stanza-id: %s, '
'message-id: %s', stanza_id, message_id)
raise nbxmpp.NodeProcessed
additional_data = AdditionalDataDict()
if properties.has_user_delay:
# Record it as a user timestamp
additional_data.set_value(
'gajim', 'user_timestamp', properties.user_timestamp)
parse_oob(properties, additional_data)
msgtxt = properties.body
if properties.is_encrypted:
additional_data['encrypted'] = properties.encrypted.additional_data
else:
if properties.eme is not None:
msgtxt = get_eme_message(properties.eme)
if not msgtxt:
# For example Chatstates, Receipts, Chatmarkers
self._log.debug(stanza.getProperties())
return
with_ = properties.jid.bare
if properties.is_muc_pm:
# we store the message with the full JID
with_ = str(with_)
if properties.is_self_message:
# Self messages can only be deduped with origin-id
if message_id is None:
self._log.warning('Self message without origin-id found')
return
stanza_id = message_id
app.storage.archive.insert_into_logs(
self._account,
with_,
properties.mam.timestamp,
kind,
unread=False,
message=msgtxt,
contact_name=properties.muc_nickname,
additional_data=additional_data,
stanza_id=stanza_id,
message_id=properties.id)
app.nec.push_incoming_event(
NetworkEvent('mam-decrypted-message-received',
account=self._account,
additional_data=additional_data,
correct_id=parse_correction(properties),
archive_jid=properties.mam.archive,
msgtxt=properties.body,
properties=properties,
kind=kind,
)
)
def _is_valid_request(self, properties):
valid_id = self._mam_query_ids.get(properties.mam.archive, None)
return valid_id == properties.mam.query_id
def _get_query_id(self, jid):
query_id = generate_id()
self._mam_query_ids[jid] = query_id
return query_id
def _get_query_params(self):
own_jid = self._con.get_own_jid().bare
archive = app.storage.archive.get_archive_infos(own_jid)
mam_id = None
if archive is not None:
mam_id = archive.last_mam_id
start_date = None
if mam_id:
self._log.info('Request archive: %s, after mam-id %s',
own_jid, mam_id)
else:
# First Start, we request the last week
start_date = datetime.utcnow() - timedelta(days=7)
self._log.info('Request archive: %s, after date %s',
own_jid, start_date)
return mam_id, start_date
def _get_muc_query_params(self, jid, threshold):
archive = app.storage.archive.get_archive_infos(jid)
mam_id = None
start_date = None
if archive is None or archive.last_mam_id is None:
# First join
start_date = datetime.utcnow() - timedelta(days=1)
self._log.info('Request archive: %s, after date %s',
jid, start_date)
elif threshold == SyncThreshold.NO_THRESHOLD:
# Not our first join and no threshold set
mam_id = archive.last_mam_id
self._log.info('Request archive: %s, after mam-id %s',
jid, archive.last_mam_id)
else:
# Not our first join, check how much time elapsed since our
# last join and check against threshold
last_timestamp = archive.last_muc_timestamp
if last_timestamp is None:
self._log.info('No last muc timestamp found: %s', jid)
last_timestamp = 0
last = datetime.utcfromtimestamp(float(last_timestamp))
if datetime.utcnow() - last > timedelta(days=threshold):
# To much time has elapsed since last join, apply threshold
start_date = datetime.utcnow() - timedelta(days=threshold)
self._log.info('Too much time elapsed since last join, '
'request archive: %s, after date %s, '
'threshold: %s', jid, start_date, threshold)
else:
# Request from last mam-id
mam_id = archive.last_mam_id
self._log.info('Request archive: %s, after mam-id %s:',
jid, archive.last_mam_id)
return mam_id, start_date
@as_task
def request_archive_on_signin(self):
_task = yield
own_jid = self._con.get_own_jid().bare
if own_jid in self._mam_query_ids:
self._log.warning('request already running for %s', own_jid)
return
mam_id, start_date = self._get_query_params()
result = yield self._execute_query(own_jid, mam_id, start_date)
if is_error(result):
if result.condition != 'item-not-found':
self._log.warning(result)
return
app.storage.archive.reset_archive_infos(result.jid)
_, start_date = self._get_query_params()
result = yield self._execute_query(result.jid, None, start_date)
if is_error(result):
self._log.warning(result)
return
if result.rsm.last is not None:
# <last> is not provided if the requested page was empty
# so this means we did not get anything hence we only need
# to update the archive info if <last> is present
app.storage.archive.set_archive_infos(
result.jid,
last_mam_id=result.rsm.last,
last_muc_timestamp=time.time())
if start_date is not None:
# Record the earliest timestamp we request from
# the account archive. For the account archive we only
# set start_date at the very first request.
app.storage.archive.set_archive_infos(
result.jid,
oldest_mam_timestamp=start_date.timestamp())
@as_task
def request_archive_on_muc_join(self, jid):
_task = yield
threshold = app.settings.get_group_chat_setting(self._account,
jid,
'sync_threshold')
self._log.info('Threshold for %s: %s', jid, threshold)
if threshold == SyncThreshold.NO_SYNC:
return
mam_id, start_date = self._get_muc_query_params(jid, threshold)
result = yield self._execute_query(jid, mam_id, start_date)
if is_error(result):
if result.condition != 'item-not-found':
self._log.warning(result)
return
app.storage.archive.reset_archive_infos(result.jid)
_, start_date = self._get_muc_query_params(jid, threshold)
result = yield self._execute_query(result.jid, None, start_date)
if is_error(result):
self._log.warning(result)
return
if result.rsm.last is not None:
# <last> is not provided if the requested page was empty
# so this means we did not get anything hence we only need
# to update the archive info if <last> is present
app.storage.archive.set_archive_infos(
result.jid,
last_mam_id=result.rsm.last,
last_muc_timestamp=time.time())
@as_task
def _execute_query(self, jid, mam_id, start_date):
_task = yield
if jid in self._catch_up_finished:
self._catch_up_finished.remove(jid)
queryid = self._get_query_id(jid)
result = yield self.make_query(jid,
queryid,
after=mam_id,
start=start_date)
self._remove_query_id(result.jid)
raise_if_error(result)
while not result.complete:
app.storage.archive.set_archive_infos(result.jid,
last_mam_id=result.rsm.last)
queryid = self._get_query_id(result.jid)
result = yield self.make_query(result.jid,
queryid,
after=result.rsm.last,
start=start_date)
self._remove_query_id(result.jid)
raise_if_error(result)
self._catch_up_finished.append(result.jid)
self._log.info('Request finished: %s, last mam id: %s',
result.jid, result.rsm.last)
yield result
def request_archive_interval(self,
start_date,
end_date,
after=None,
queryid=None):
jid = self._con.get_own_jid().bare
if after is None:
self._log.info('Request interval: %s, from %s to %s',
jid, start_date, end_date)
else:
self._log.info('Request page: %s, after %s', jid, after)
if queryid is None:
queryid = self._get_query_id(jid)
self._mam_query_ids[jid] = queryid
self.make_query(jid,
queryid,
after=after,
start=start_date,
end=end_date,
callback=self._on_interval_result,
user_data=(queryid, start_date, end_date))
return queryid
def _on_interval_result(self, task):
queryid, start_date, end_date = task.get_user_data()
try:
result = task.finish()
except (StanzaError, MalformedStanzaError) as error:
self._remove_query_id(error.jid)
return
self._remove_query_id(result.jid)
if start_date:
timestamp = start_date.timestamp()
else:
timestamp = ArchiveState.ALL
if result.complete:
self._log.info('Request finished: %s, last mam id: %s',
result.jid, result.rsm.last)
app.storage.archive.set_archive_infos(
result.jid, oldest_mam_timestamp=timestamp)
app.nec.push_incoming_event(NetworkEvent(
'archiving-interval-finished',
account=self._account,
query_id=queryid))
else:
self.request_archive_interval(start_date,
end_date,
result.rsm.last,
queryid)
def get_instance(*args, **kwargs):
return MAM(*args, **kwargs), 'MAM'

View File

@ -0,0 +1,390 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# Message handler
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import should_log
from gajim.common.const import KindConstant
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import get_eme_message
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_xhtml
class Message(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._check_if_unknown_contact,
priority=41),
StanzaHandler(name='message',
callback=self._message_received,
priority=50),
StanzaHandler(name='message',
typ='error',
callback=self._message_error_received,
priority=50),
]
# XEPs for which this message module should not be executed
self._message_namespaces = set([Namespace.ROSTERX,
Namespace.IBB])
def _check_if_unknown_contact(self, _con, stanza, properties):
if (properties.type.is_groupchat or
properties.is_muc_pm or
properties.is_self_message or
properties.is_mam_message):
return
if self._con.get_own_jid().domain == str(properties.jid):
# Server message
return
if not app.settings.get_account_setting(self._account,
'ignore_unknown_contacts'):
return
jid = properties.jid.bare
if self._con.get_module('Roster').get_item(jid) is None:
self._log.warning('Ignore message from unknown contact: %s', jid)
self._log.warning(stanza)
raise nbxmpp.NodeProcessed
def _message_received(self, _con, stanza, properties):
if (properties.is_mam_message or
properties.is_pubsub or
properties.type.is_error):
return
# Check if a child of the message contains any
# namespaces that we handle in other modules.
# nbxmpp executes less common handlers last
if self._message_namespaces & set(stanza.getProperties()):
return
self._log.info('Received from %s', stanza.getFrom())
app.nec.push_incoming_event(NetworkEvent(
'raw-message-received',
conn=self._con,
stanza=stanza,
account=self._account))
if properties.is_carbon_message and properties.carbon.is_sent:
# Ugly, we treat the from attr as the remote jid,
# to make that work with sent carbons we have to do this.
# TODO: Check where in Gajim and plugins we depend on that behavior
stanza.setFrom(stanza.getTo())
from_ = stanza.getFrom()
fjid = str(from_)
jid = from_.bare
resource = from_.resource
type_ = properties.type
stanza_id, message_id = self._get_unique_id(properties)
if properties.type.is_groupchat and properties.has_server_delay:
# Only for XEP-0045 MUC History
# Dont check for message text because the message could be
# encrypted.
if app.storage.archive.deduplicate_muc_message(
self._account,
properties.jid.bare,
properties.jid.resource,
properties.timestamp,
properties.id):
raise nbxmpp.NodeProcessed
if (properties.is_self_message or properties.is_muc_pm):
archive_jid = self._con.get_own_jid().bare
if app.storage.archive.find_stanza_id(
self._account,
archive_jid,
stanza_id,
message_id,
properties.type.is_groupchat):
return
msgtxt = properties.body
# TODO: remove all control UI stuff
gc_control = app.interface.msg_win_mgr.get_gc_control(
jid, self._account)
if not gc_control:
minimized = app.interface.minimized_controls[self._account]
gc_control = minimized.get(jid)
session = None
if not properties.type.is_groupchat:
if properties.is_muc_pm and properties.type.is_error:
session = self._con.find_session(fjid, properties.thread)
if not session:
session = self._con.get_latest_session(fjid)
if not session:
session = self._con.make_new_session(
fjid, properties.thread, type_='pm')
else:
session = self._con.get_or_create_session(
fjid, properties.thread)
if properties.thread and not session.received_thread_id:
session.received_thread_id = True
session.last_receive = time.time()
additional_data = AdditionalDataDict()
if properties.has_user_delay:
additional_data.set_value(
'gajim', 'user_timestamp', properties.user_timestamp)
parse_oob(properties, additional_data)
parse_xhtml(properties, additional_data)
app.nec.push_incoming_event(NetworkEvent('update-client-info',
account=self._account,
jid=jid,
resource=resource))
if properties.is_encrypted:
additional_data['encrypted'] = properties.encrypted.additional_data
else:
if properties.eme is not None:
msgtxt = get_eme_message(properties.eme)
displaymarking = None
if properties.has_security_label:
displaymarking = properties.security_label.displaymarking
event_attr = {
'conn': self._con,
'stanza': stanza,
'account': self._account,
'additional_data': additional_data,
'fjid': fjid,
'jid': jid,
'resource': resource,
'stanza_id': stanza_id,
'unique_id': stanza_id or message_id,
'correct_id': parse_correction(properties),
'msgtxt': msgtxt,
'session': session,
'delayed': properties.user_timestamp is not None,
'gc_control': gc_control,
'popup': False,
'msg_log_id': None,
'displaymarking': displaymarking,
'properties': properties,
}
if type_.is_groupchat:
if not msgtxt:
return
event_attr.update({
'room_jid': jid,
})
event = NetworkEvent('gc-message-received', **event_attr)
app.nec.push_incoming_event(event)
# TODO: Some plugins modify msgtxt in the GUI event
self._log_muc_message(event)
return
app.nec.push_incoming_event(
NetworkEvent('decrypted-message-received', **event_attr))
def _message_error_received(self, _con, _stanza, properties):
jid = properties.jid
if not properties.is_muc_pm:
jid = jid.new_as_bare()
self._log.info(properties.error)
app.storage.archive.set_message_error(
app.get_jid_from_account(self._account),
jid,
properties.id,
properties.error)
app.nec.push_incoming_event(
NetworkEvent('message-error',
account=self._account,
jid=jid,
room_jid=jid,
message_id=properties.id,
error=properties.error))
def _log_muc_message(self, event):
self._check_for_mam_compliance(event.room_jid, event.stanza_id)
if (should_log(self._account, event.jid) and
event.msgtxt and event.properties.muc_nickname):
# if not event.nick, it means message comes from room itself
# usually it hold description and can be send at each connection
# so don't store it in logs
app.storage.archive.insert_into_logs(
self._account,
event.jid,
event.properties.timestamp,
KindConstant.GC_MSG,
message=event.msgtxt,
contact_name=event.properties.muc_nickname,
additional_data=event.additional_data,
stanza_id=event.stanza_id,
message_id=event.properties.id)
def _check_for_mam_compliance(self, room_jid, stanza_id):
disco_info = app.storage.cache.get_last_disco_info(room_jid)
if stanza_id is None and disco_info.mam_namespace == Namespace.MAM_2:
self._log.warning('%s announces mam:2 without stanza-id', room_jid)
def _get_unique_id(self, properties):
if properties.is_self_message:
# Deduplicate self message with message-id
return None, properties.id
if properties.stanza_id is None:
return None, None
if properties.type.is_groupchat:
disco_info = app.storage.cache.get_last_disco_info(
properties.jid.bare)
if disco_info.mam_namespace != Namespace.MAM_2:
return None, None
archive = properties.jid
else:
if not self._con.get_module('MAM').available:
return None, None
archive = self._con.get_own_jid()
if archive.bare_match(properties.stanza_id.by):
return properties.stanza_id.id, None
# stanza-id not added by the archive, ignore it.
return None, None
def build_message_stanza(self, message):
own_jid = self._con.get_own_jid()
stanza = nbxmpp.Message(to=message.jid,
body=message.message,
typ=message.type_,
subject=message.subject,
xhtml=message.xhtml)
if message.correct_id:
stanza.setTag('replace', attrs={'id': message.correct_id},
namespace=Namespace.CORRECT)
# XEP-0359
message.message_id = generate_id()
stanza.setID(message.message_id)
stanza.setOriginID(message.message_id)
if message.label:
stanza.addChild(node=message.label.to_node())
# XEP-0172: user_nickname
if message.user_nick:
stanza.setTag('nick', namespace=Namespace.NICK).setData(
message.user_nick)
# XEP-0203
# TODO: Seems delayed is not set anywhere
if message.delayed:
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ',
time.gmtime(message.delayed))
stanza.addChild('delay',
namespace=Namespace.DELAY2,
attrs={'from': str(own_jid), 'stamp': timestamp})
# XEP-0224
if message.attention:
stanza.setTag('attention', namespace=Namespace.ATTENTION)
# XEP-0066
if message.oob_url is not None:
oob = stanza.addChild('x', namespace=Namespace.X_OOB)
oob.addChild('url').setData(message.oob_url)
# XEP-0184
if not own_jid.bare_match(message.jid):
if message.message and not message.is_groupchat:
stanza.setReceiptRequest()
# Mark Message as MUC PM
if message.contact.is_pm_contact:
stanza.setTag('x', namespace=Namespace.MUC_USER)
# XEP-0085
if message.chatstate is not None:
stanza.setTag(message.chatstate, namespace=Namespace.CHATSTATES)
if not message.message:
stanza.setTag('no-store',
namespace=Namespace.MSG_HINTS)
# XEP-0333
if message.message:
stanza.setMarkable()
if message.marker:
marker, id_ = message.marker
stanza.setMarker(marker, id_)
# Add other nodes
if message.nodes is not None:
for node in message.nodes:
stanza.addChild(node=node)
return stanza
def log_message(self, message):
if not message.is_loggable:
return
if not should_log(self._account, message.jid):
return
if message.message is None:
return
app.storage.archive.insert_into_logs(
self._account,
message.jid,
message.timestamp,
message.kind,
message=message.message,
subject=message.subject,
additional_data=message.additional_data,
message_id=message.message_id,
stanza_id=message.message_id)
def get_instance(*args, **kwargs):
return Message(*args, **kwargs), 'Message'

View File

@ -0,0 +1,107 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0209: Metacontacts
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import helpers
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class MetaContacts(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.available = False
def get_metacontacts(self):
if not app.settings.get('metacontacts_enabled'):
self._con.connect_machine()
return
self._log.info('Request')
node = nbxmpp.Node('storage', attrs={'xmlns': 'storage:metacontacts'})
iq = nbxmpp.Iq('get', Namespace.PRIVATE, payload=node)
self._con.connection.SendAndCallForResponse(
iq, self._metacontacts_received)
def _metacontacts_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Request error: %s', stanza.getError())
else:
self.available = True
meta_list = self._parse_metacontacts(stanza)
self._log.info('Received: %s', meta_list)
app.nec.push_incoming_event(NetworkEvent(
'metacontacts-received', conn=self._con, meta_list=meta_list))
self._con.connect_machine()
@staticmethod
def _parse_metacontacts(stanza):
meta_list = {}
query = stanza.getQuery()
storage = query.getTag('storage')
metas = storage.getTags('meta')
for meta in metas:
try:
jid = helpers.parse_jid(meta.getAttr('jid'))
except helpers.InvalidFormat:
continue
tag = meta.getAttr('tag')
data = {'jid': jid}
order = meta.getAttr('order')
try:
order = int(order)
except Exception:
order = 0
if order is not None:
data['order'] = order
if tag in meta_list:
meta_list[tag].append(data)
else:
meta_list[tag] = [data]
return meta_list
def store_metacontacts(self, tags_list):
if not app.account_is_available(self._account):
return
iq = nbxmpp.Iq('set', Namespace.PRIVATE)
meta = iq.getQuery().addChild('storage',
namespace='storage:metacontacts')
for tag in tags_list:
for data in tags_list[tag]:
jid = data['jid']
dict_ = {'jid': jid, 'tag': tag}
if 'order' in data:
dict_['order'] = data['order']
meta.addChild(name='meta', attrs=dict_)
self._log.info('Store: %s', tags_list)
self._con.connection.SendAndCallForResponse(
iq, self._store_response_received)
def _store_response_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Store error: %s', stanza.getError())
def get_instance(*args, **kwargs):
return MetaContacts(*args, **kwargs), 'MetaContacts'

View File

@ -0,0 +1,51 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# All XEPs that dont need their own module
import logging
from gajim.common.i18n import get_rfc5646_lang
log = logging.getLogger('gajim.c.m.misc')
# XEP-0066: Out of Band Data
def parse_oob(properties, additional_data):
if not properties.is_oob:
return
additional_data.set_value('gajim', 'oob_url', properties.oob.url)
if properties.oob.desc is not None:
additional_data.set_value('gajim', 'oob_desc',
properties.oob.desc)
# XEP-0308: Last Message Correction
def parse_correction(properties):
if not properties.is_correction:
return None
return properties.correction.id
# XEP-0071: XHTML-IM
def parse_xhtml(properties, additional_data):
if not properties.has_xhtml:
return
body = properties.xhtml.get_body(get_rfc5646_lang())
additional_data.set_value('gajim', 'xhtml', body)

857
gajim/common/modules/muc.py Normal file
View File

@ -0,0 +1,857 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0045: Multi-User Chat
# XEP-0249: Direct MUC Invitations
import logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.const import InviteType
from nbxmpp.const import PresenceType
from nbxmpp.const import StatusCode
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from gi.repository import GLib
from gajim.common import app
from gajim.common import helpers
from gajim.common import ged
from gajim.common.const import KindConstant
from gajim.common.const import MUCJoinedState
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import get_default_muc_config
from gajim.common.helpers import to_user_string
from gajim.common.helpers import event_filter
from gajim.common.nec import NetworkEvent
from gajim.common.modules.bits_of_binary import store_bob_data
from gajim.common.modules.base import BaseModule
log = logging.getLogger('gajim.c.m.muc')
class MUC(BaseModule):
_nbxmpp_extends = 'MUC'
_nbxmpp_methods = [
'get_affiliation',
'set_role',
'set_affiliation',
'set_config',
'set_subject',
'cancel_config',
'send_captcha',
'cancel_captcha',
'decline',
'invite',
'request_config',
'request_voice',
'approve_voice_request',
'destroy',
'request_disco_info'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._on_muc_user_presence,
ns=Namespace.MUC_USER,
priority=49),
StanzaHandler(name='presence',
callback=self._on_error_presence,
typ='error',
priority=49),
StanzaHandler(name='message',
callback=self._on_subject_change,
typ='groupchat',
priority=49),
StanzaHandler(name='message',
callback=self._on_config_change,
ns=Namespace.MUC_USER,
priority=49),
StanzaHandler(name='message',
callback=self._on_invite_or_decline,
typ='normal',
ns=Namespace.MUC_USER,
priority=49),
StanzaHandler(name='message',
callback=self._on_invite_or_decline,
ns=Namespace.CONFERENCE,
priority=49),
StanzaHandler(name='message',
callback=self._on_captcha_challenge,
ns=Namespace.CAPTCHA,
priority=49),
StanzaHandler(name='message',
callback=self._on_voice_request,
ns=Namespace.DATA,
priority=49)
]
self.register_events([
('account-disconnected', ged.CORE, self._on_account_disconnected),
])
self._manager = MUCManager(self._log)
self._rejoin_muc = set()
self._join_timeouts = {}
self._rejoin_timeouts = {}
self._muc_service_jid = None
@property
def supported(self):
return self._muc_service_jid is not None
@property
def service_jid(self):
return self._muc_service_jid
def get_manager(self):
return self._manager
def pass_disco(self, info):
for identity in info.identities:
if identity.category != 'conference':
continue
if identity.type != 'text':
continue
if Namespace.MUC in info.features:
self._log.info('Discovered MUC: %s', info.jid)
self._muc_service_jid = info.jid
raise nbxmpp.NodeProcessed
def join(self, muc_data):
if not app.account_is_available(self._account):
return
self._manager.add(muc_data)
disco_info = app.storage.cache.get_last_disco_info(muc_data.jid,
max_age=60)
if disco_info is None:
self._con.get_module('Discovery').disco_muc(
muc_data.jid,
callback=self._on_disco_result)
else:
self._join(muc_data)
def create(self, muc_data):
if not app.account_is_available(self._account):
return
self._manager.add(muc_data)
self._create(muc_data)
def _on_disco_result(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
app.nec.push_incoming_event(
NetworkEvent('muc-join-failed',
account=self._account,
room_jid=error.jid.bare,
error=error))
return
muc_data = self._manager.get(result.info.jid)
if muc_data is None:
self._log.warning('MUC Data not found, join aborted')
return
self._join(muc_data)
def _join(self, muc_data):
presence = self._con.get_module('Presence').get_presence(
muc_data.occupant_jid,
show=self._con.status,
status=self._con.status_message)
muc_x = presence.setTag(Namespace.MUC + ' x')
muc_x.setTag('history', {'maxchars': '0'})
if muc_data.password is not None:
muc_x.setTagData('password', muc_data.password)
self._log.info('Join MUC: %s', muc_data.jid)
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINING)
self._con.connection.send(presence)
def _rejoin(self, room_jid):
muc_data = self._manager.get(room_jid)
if muc_data.state == MUCJoinedState.NOT_JOINED:
self._log.info('Rejoin %s', room_jid)
self._join(muc_data)
return True
def _create(self, muc_data):
presence = self._con.get_module('Presence').get_presence(
muc_data.occupant_jid,
show=self._con.status,
status=self._con.status_message)
presence.setTag(Namespace.MUC + ' x')
self._log.info('Create MUC: %s', muc_data.jid)
self._manager.set_state(muc_data.jid, MUCJoinedState.CREATING)
self._con.connection.send(presence)
def leave(self, room_jid, reason=None):
self._log.info('Leave MUC: %s', room_jid)
self._remove_join_timeout(room_jid)
self._remove_rejoin_timeout(room_jid)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
muc_data = self._manager.get(room_jid)
self._con.get_module('Presence').send_presence(
muc_data.occupant_jid,
typ='unavailable',
status=reason,
caps=False)
# We leave a group chat, disable bookmark autojoin
self._con.get_module('Bookmarks').modify(room_jid, autojoin=False)
def configure_room(self, room_jid):
self._nbxmpp('MUC').request_config(room_jid,
callback=self._on_room_config)
def _on_room_config(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info(error)
app.nec.push_incoming_event(NetworkEvent(
'muc-configuration-failed',
account=self._account,
room_jid=error.jid,
error=error))
return
self._log.info('Configure room: %s', result.jid)
muc_data = self._manager.get(result.jid)
self._apply_config(result.form, muc_data.config)
self.set_config(result.jid,
result.form,
callback=self._on_config_result)
@staticmethod
def _apply_config(form, config=None):
default_config = get_default_muc_config()
if config is not None:
default_config.update(config)
for var, value in default_config.items():
try:
field = form[var]
except KeyError:
pass
else:
field.value = value
def _on_config_result(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info(error)
app.nec.push_incoming_event(NetworkEvent(
'muc-configuration-failed',
account=self._account,
room_jid=error.jid,
error=error))
return
self._con.get_module('Discovery').disco_muc(
result.jid, callback=self._on_disco_result_after_config)
# If this is an automatic room creation
try:
invites = app.automatic_rooms[self._account][result.jid]['invities']
except KeyError:
return
user_list = {}
for jid in invites:
user_list[jid] = {'affiliation': 'member'}
self.set_affiliation(result.jid, user_list)
for jid in invites:
self.invite(result.jid, jid)
def _on_disco_result_after_config(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
return
jid = result.info.jid
muc_data = self._manager.get(jid)
self._room_join_complete(muc_data)
self._log.info('Configuration finished: %s', jid)
app.nec.push_incoming_event(NetworkEvent(
'muc-configuration-finished',
account=self._account,
room_jid=jid))
def update_presence(self):
mucs = self._manager.get_mucs_with_state([MUCJoinedState.JOINED,
MUCJoinedState.JOINING])
status, message, idle = self._con.get_presence_state()
for muc_data in mucs:
self._con.get_module('Presence').send_presence(
muc_data.occupant_jid,
show=status,
status=message,
idle_time=idle)
def change_nick(self, room_jid, new_nick):
status, message, _idle = self._con.get_presence_state()
self._con.get_module('Presence').send_presence(
'%s/%s' % (room_jid, new_nick),
show=status,
status=message)
def _on_error_presence(self, _con, _stanza, properties):
room_jid = properties.jid.bare
muc_data = self._manager.get(room_jid)
if muc_data is None:
return
if muc_data.state == MUCJoinedState.JOINING:
if properties.error.condition == 'conflict':
self._remove_rejoin_timeout(room_jid)
muc_data.nick += '_'
self._log.info('Nickname conflict: %s change to %s',
muc_data.jid, muc_data.nick)
self._join(muc_data)
elif properties.error.condition == 'not-authorized':
self._remove_rejoin_timeout(room_jid)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
self._raise_muc_event('muc-password-required', properties)
else:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
if room_jid not in self._rejoin_muc:
app.nec.push_incoming_event(
NetworkEvent('muc-join-failed',
account=self._account,
room_jid=room_jid,
error=properties.error))
elif muc_data.state == MUCJoinedState.CREATING:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
app.nec.push_incoming_event(
NetworkEvent('muc-creation-failed',
account=self._account,
room_jid=room_jid,
error=properties.error))
elif muc_data.state == MUCJoinedState.CAPTCHA_REQUEST:
app.nec.push_incoming_event(
NetworkEvent('muc-captcha-error',
account=self._account,
room_jid=room_jid,
error_text=to_user_string(properties.error)))
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
elif muc_data.state == MUCJoinedState.CAPTCHA_FAILED:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
else:
self._raise_muc_event('muc-presence-error', properties)
def _on_muc_user_presence(self, _con, stanza, properties):
if properties.type == PresenceType.ERROR:
return
room_jid = str(properties.muc_jid)
if room_jid not in self._manager:
self._log.warning('Presence from unknown MUC')
self._log.warning(stanza)
return
muc_data = self._manager.get(room_jid)
if properties.is_muc_destroyed:
for contact in app.contacts.get_gc_contact_list(
self._account, room_jid):
contact.presence = PresenceType.UNAVAILABLE
self._log.info('MUC destroyed: %s', room_jid)
self._remove_join_timeout(room_jid)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
self._raise_muc_event('muc-destroyed', properties)
return
contact = app.contacts.get_gc_contact(self._account,
room_jid,
properties.muc_nickname)
if properties.is_nickname_changed:
if properties.is_muc_self_presence:
muc_data.nick = properties.muc_user.nick
self._con.get_module('Bookmarks').modify(muc_data.jid,
nick=muc_data.nick)
app.contacts.remove_gc_contact(self._account, contact)
contact.name = properties.muc_user.nick
app.contacts.add_gc_contact(self._account, contact)
initiator = 'Server' if properties.is_nickname_modified else 'User'
self._log.info('%s nickname changed: %s to %s',
initiator,
properties.jid,
properties.muc_user.nick)
self._raise_muc_event('muc-nickname-changed', properties)
return
if contact is None and properties.type.is_available:
self._add_new_muc_contact(properties)
if properties.is_muc_self_presence:
self._log.info('Self presence: %s', properties.jid)
if muc_data.state == MUCJoinedState.JOINING:
if (properties.is_nickname_modified or
muc_data.nick != properties.muc_nickname):
muc_data.nick = properties.muc_nickname
self._log.info('Server modified nickname to: %s',
properties.muc_nickname)
elif muc_data.state == MUCJoinedState.CREATING:
if properties.is_new_room:
self.configure_room(room_jid)
self._start_join_timeout(room_jid)
self._raise_muc_event('muc-self-presence', properties)
else:
self._log.info('User joined: %s', properties.jid)
self._raise_muc_event('muc-user-joined', properties)
return
if properties.is_muc_self_presence and properties.is_kicked:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
self._raise_muc_event('muc-self-kicked', properties)
status_codes = properties.muc_status_codes or []
if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
self._start_rejoin_timeout(room_jid)
return
if properties.is_muc_self_presence and properties.type.is_unavailable:
# Its not a kick, so this is the reflection of our own
# unavailable presence, because we left the MUC
return
if properties.type.is_unavailable:
for _event in app.events.get_events(self._account,
jid=str(properties.jid),
types=['pm']):
contact.show = properties.show
contact.presence = properties.type
contact.status = properties.status
contact.affiliation = properties.affiliation
app.interface.handle_event(self._account,
str(properties.jid),
'pm')
# Handle only the first pm event, the rest will be
# handled by the opened ChatControl
break
if contact is None:
# If contact is None, its probably that a user left from a not
# insync MUC, can happen on older servers
self._log.warning('Unknown contact left groupchat: %s',
properties.jid)
else:
# We remove the contact from the MUC, but there could be
# a PrivateChatControl open, so we update the contacts presence
contact.presence = properties.type
app.contacts.remove_gc_contact(self._account, contact)
self._log.info('User %s left', properties.jid)
self._raise_muc_event('muc-user-left', properties)
return
if contact.affiliation != properties.affiliation:
contact.affiliation = properties.affiliation
self._log.info('Affiliation changed: %s %s',
properties.jid,
properties.affiliation)
self._raise_muc_event('muc-user-affiliation-changed', properties)
if contact.role != properties.role:
contact.role = properties.role
self._log.info('Role changed: %s %s',
properties.jid,
properties.role)
self._raise_muc_event('muc-user-role-changed', properties)
if (contact.status != properties.status or
contact.show != properties.show):
contact.status = properties.status
contact.show = properties.show
self._log.info('Show/Status changed: %s %s %s',
properties.jid,
properties.status,
properties.show)
self._raise_muc_event('muc-user-status-show-changed', properties)
def _start_rejoin_timeout(self, room_jid):
self._remove_rejoin_timeout(room_jid)
self._rejoin_muc.add(room_jid)
self._log.info('Start rejoin timeout for: %s', room_jid)
id_ = GLib.timeout_add_seconds(2, self._rejoin, room_jid)
self._rejoin_timeouts[room_jid] = id_
def _remove_rejoin_timeout(self, room_jid):
self._rejoin_muc.discard(room_jid)
id_ = self._rejoin_timeouts.get(room_jid)
if id_ is not None:
self._log.info('Remove rejoin timeout for: %s', room_jid)
GLib.source_remove(id_)
del self._rejoin_timeouts[room_jid]
def _start_join_timeout(self, room_jid):
self._remove_join_timeout(room_jid)
self._log.info('Start join timeout for: %s', room_jid)
id_ = GLib.timeout_add_seconds(
10, self._fake_subject_change, room_jid)
self._join_timeouts[room_jid] = id_
def _remove_join_timeout(self, room_jid):
id_ = self._join_timeouts.get(room_jid)
if id_ is not None:
self._log.info('Remove join timeout for: %s', room_jid)
GLib.source_remove(id_)
del self._join_timeouts[room_jid]
def _raise_muc_event(self, event_name, properties):
app.nec.push_incoming_event(
NetworkEvent(event_name,
account=self._account,
room_jid=properties.jid.bare,
properties=properties))
self._log_muc_event(event_name, properties)
def _log_muc_event(self, event_name, properties):
if event_name not in ['muc-user-joined',
'muc-user-left',
'muc-user-status-show-changed']:
return
if (not app.settings.get('log_contact_status_changes') or
not helpers.should_log(self._account, properties.jid)):
return
additional_data = AdditionalDataDict()
if properties.muc_user is not None:
if properties.muc_user.jid is not None:
additional_data.set_value(
'gajim', 'real_jid', str(properties.muc_user.jid))
# TODO: Refactor
if properties.type == PresenceType.UNAVAILABLE:
show = 'offline'
else:
show = properties.show.value
show = app.storage.archive.convert_show_values_to_db_api_values(show)
app.storage.archive.insert_into_logs(
self._account,
properties.jid.bare,
properties.timestamp,
KindConstant.GCSTATUS,
contact_name=properties.muc_nickname,
message=properties.status or None,
show=show,
additional_data=additional_data)
def _add_new_muc_contact(self, properties):
real_jid = None
if properties.muc_user.jid is not None:
real_jid = str(properties.muc_user.jid)
contact = app.contacts.create_gc_contact(
room_jid=properties.jid.bare,
account=self._account,
name=properties.muc_nickname,
show=properties.show,
status=properties.status,
presence=properties.type,
role=properties.role,
affiliation=properties.affiliation,
jid=real_jid,
avatar_sha=properties.avatar_sha)
app.contacts.add_gc_contact(self._account, contact)
def _on_subject_change(self, _con, _stanza, properties):
if not properties.is_muc_subject:
return
self._handle_subject_change(str(properties.muc_jid),
properties.subject,
properties.muc_nickname,
properties.user_timestamp)
raise nbxmpp.NodeProcessed
def _fake_subject_change(self, room_jid):
# This is for servers which dont send empty subjects as part of the
# event order on joining a MUC. For example jabber.ru
self._log.warning('Fake subject received for %s', room_jid)
del self._join_timeouts[room_jid]
self._handle_subject_change(room_jid, None, None, None)
def _handle_subject_change(self, room_jid, subject, nickname, timestamp):
contact = app.contacts.get_groupchat_contact(self._account, room_jid)
if contact is None:
return
contact.status = subject
app.nec.push_incoming_event(
NetworkEvent('muc-subject',
account=self._account,
room_jid=room_jid,
subject=subject,
nickname=nickname,
user_timestamp=timestamp,
is_fake=subject is None))
muc_data = self._manager.get(room_jid)
if muc_data.state == MUCJoinedState.JOINING:
self._room_join_complete(muc_data)
app.nec.push_incoming_event(
NetworkEvent('muc-joined',
account=self._account,
room_jid=muc_data.jid))
def _room_join_complete(self, muc_data):
self._remove_join_timeout(muc_data.jid)
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINED)
self._remove_rejoin_timeout(muc_data.jid)
# We successfully joined a MUC, set add bookmark with autojoin
self._con.get_module('Bookmarks').add_or_modify(
muc_data.jid,
autojoin=True,
password=muc_data.password,
nick=muc_data.nick)
def _on_voice_request(self, _con, _stanza, properties):
if not properties.is_voice_request:
return
jid = str(properties.jid)
contact = app.contacts.get_groupchat_contact(self._account, jid)
if contact is None:
return
app.nec.push_incoming_event(
NetworkEvent('muc-voice-request',
account=self._account,
room_jid=str(properties.muc_jid),
voice_request=properties.voice_request))
raise nbxmpp.NodeProcessed
def _on_captcha_challenge(self, _con, _stanza, properties):
if not properties.is_captcha_challenge:
return
if properties.is_mam_message:
# Some servers store captcha challenges in MAM, dont process them
self._log.warning('Ignore captcha challenge received from MAM')
raise nbxmpp.NodeProcessed
muc_data = self._manager.get(properties.jid)
if muc_data is None:
return
if muc_data.state != MUCJoinedState.JOINING:
self._log.warning('Received captcha request but state != %s',
MUCJoinedState.JOINING)
return
contact = app.contacts.get_groupchat_contact(self._account,
str(properties.jid))
if contact is None:
return
self._log.info('Captcha challenge received from %s', properties.jid)
store_bob_data(properties.captcha.bob_data)
muc_data.captcha_id = properties.id
self._manager.set_state(properties.jid, MUCJoinedState.CAPTCHA_REQUEST)
self._remove_rejoin_timeout(properties.jid)
app.nec.push_incoming_event(
NetworkEvent('muc-captcha-challenge',
account=self._account,
room_jid=properties.jid.bare,
form=properties.captcha.form))
raise nbxmpp.NodeProcessed
def cancel_captcha(self, room_jid):
muc_data = self._manager.get(room_jid)
if muc_data is None:
return
if muc_data.captcha_id is None:
self._log.warning('No captcha message id available')
return
self._nbxmpp('MUC').cancel_captcha(room_jid, muc_data.captcha_id)
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
def send_captcha(self, room_jid, form_node):
self._manager.set_state(room_jid, MUCJoinedState.JOINING)
self._nbxmpp('MUC').send_captcha(room_jid,
form_node,
callback=self._on_captcha_result)
def _on_captcha_result(self, task):
try:
task.finish()
except StanzaError as error:
muc_data = self._manager.get(error.jid)
if muc_data is None:
return
self._manager.set_state(error.jid, MUCJoinedState.CAPTCHA_FAILED)
app.nec.push_incoming_event(
NetworkEvent('muc-captcha-error',
account=self._account,
room_jid=str(error.jid),
error_text=to_user_string(error)))
def _on_config_change(self, _con, _stanza, properties):
if not properties.is_muc_config_change:
return
room_jid = str(properties.muc_jid)
self._log.info('Received config change: %s %s',
room_jid, properties.muc_status_codes)
app.nec.push_incoming_event(
NetworkEvent('muc-config-changed',
account=self._account,
room_jid=room_jid,
status_codes=properties.muc_status_codes))
raise nbxmpp.NodeProcessed
def _on_invite_or_decline(self, _con, _stanza, properties):
if properties.muc_decline is not None:
data = properties.muc_decline
if helpers.ignore_contact(self._account, data.from_):
raise nbxmpp.NodeProcessed
self._log.info('Invite declined from: %s, reason: %s',
data.from_, data.reason)
app.nec.push_incoming_event(
NetworkEvent('muc-decline',
account=self._account,
**data._asdict()))
raise nbxmpp.NodeProcessed
if properties.muc_invite is not None:
data = properties.muc_invite
if helpers.ignore_contact(self._account, data.from_):
raise nbxmpp.NodeProcessed
self._log.info('Invite from: %s, to: %s', data.from_, data.muc)
if app.in_groupchat(self._account, data.muc):
# We are already in groupchat. Ignore invitation
self._log.info('We are already in this room')
raise nbxmpp.NodeProcessed
self._con.get_module('Discovery').disco_muc(
data.muc,
request_vcard=True,
callback=self._on_disco_result_after_invite,
user_data=data)
raise nbxmpp.NodeProcessed
def _on_disco_result_after_invite(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning(error)
return
invite_data = task.get_user_data()
app.nec.push_incoming_event(
NetworkEvent('muc-invitation',
account=self._account,
info=result.info,
**invite_data._asdict()))
def invite(self, room, to, reason=None, continue_=False):
type_ = InviteType.MEDIATED
contact = app.contacts.get_contact_from_full_jid(self._account, to)
if contact and contact.supports(Namespace.CONFERENCE):
type_ = InviteType.DIRECT
password = app.gc_passwords.get(room, None)
self._log.info('Invite %s to %s', to, room)
return self._nbxmpp('MUC').invite(room, to, reason, password,
continue_, type_)
@event_filter(['account'])
def _on_account_disconnected(self, _event):
for room_jid in list(self._rejoin_timeouts.keys()):
self._remove_rejoin_timeout(room_jid)
for room_jid in list(self._join_timeouts.keys()):
self._remove_join_timeout(room_jid)
class MUCManager:
def __init__(self, logger):
self._log = logger
self._mucs = {}
def add(self, muc):
self._mucs[muc.jid] = muc
def remove(self, muc):
self._mucs.pop(muc.jid, None)
def get(self, room_jid):
return self._mucs.get(room_jid)
def set_state(self, room_jid, state):
muc = self._mucs.get(room_jid)
if muc is not None:
if muc.state == state:
return
self._log.info('Set MUC state: %s %s', room_jid, state)
muc.state = state
def get_joined_mucs(self):
mucs = self._mucs.values()
return [muc.jid for muc in mucs if muc.state == MUCJoinedState.JOINED]
def get_mucs_with_state(self, states):
return [muc for muc in self._mucs.values() if muc.state in states]
def reset_state(self):
for muc in self._mucs.values():
self.set_state(muc.jid, MUCJoinedState.NOT_JOINED)
def __contains__(self, room_jid):
return room_jid in self._mucs
def get_instance(*args, **kwargs):
return MUC(*args, **kwargs), 'MUC'

View File

@ -0,0 +1,39 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0163: Personal Eventing Protocol
from typing import Any
from typing import Tuple
from gajim.common.types import ConnectionT
from gajim.common.modules.base import BaseModule
class PEP(BaseModule):
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self.supported = False
def pass_disco(self, info):
for identity in info.identities:
if identity.category == 'pubsub':
if identity.type == 'pep':
self._log.info('Discovered PEP support: %s', info.jid)
self.supported = True
def get_instance(*args: Any, **kwargs: Any) -> Tuple[PEP, str]:
return PEP(*args, **kwargs), 'PEP'

View File

@ -0,0 +1,82 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0199: XMPP Ping
from typing import Any
from typing import Tuple
from typing import Generator
import time
from nbxmpp.errors import is_error
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.types import ConnectionT
from gajim.common.types import ContactsT
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class Ping(BaseModule):
_nbxmpp_extends = 'Ping'
_nbxmpp_methods = [
'ping',
]
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self.handlers = []
@as_task
def send_ping(self, contact: ContactsT) -> Generator:
_task = yield
if not app.account_is_available(self._account):
return
jid = contact.get_full_jid()
self._log.info('Send ping to %s', jid)
app.nec.push_incoming_event(NetworkEvent('ping-sent',
account=self._account,
contact=contact))
ping_time = time.time()
response = yield self.ping(jid, timeout=10)
if is_error(response):
app.nec.push_incoming_event(NetworkEvent(
'ping-error',
account=self._account,
contact=contact,
error=str(response)))
return
diff = round(time.time() - ping_time, 2)
self._log.info('Received pong from %s after %s seconds',
response.jid, diff)
app.nec.push_incoming_event(NetworkEvent('ping-reply',
account=self._account,
contact=contact,
seconds=diff))
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Ping, str]:
return Ping(*args, **kwargs), 'Ping'

View File

@ -0,0 +1,394 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# Presence handler
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import PresenceType
from gajim.common import app
from gajim.common import idle
from gajim.common.i18n import _
from gajim.common.nec import NetworkEvent
from gajim.common.helpers import should_log
from gajim.common.const import KindConstant
from gajim.common.const import ShowConstant
from gajim.common.modules.base import BaseModule
class Presence(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._presence_received,
priority=50),
StanzaHandler(name='presence',
callback=self._subscribe_received,
typ='subscribe',
priority=49),
StanzaHandler(name='presence',
callback=self._subscribed_received,
typ='subscribed',
priority=49),
StanzaHandler(name='presence',
callback=self._unsubscribe_received,
typ='unsubscribe',
priority=49),
StanzaHandler(name='presence',
callback=self._unsubscribed_received,
typ='unsubscribed',
priority=49),
]
# keep the jids we auto added (transports contacts) to not send the
# SUBSCRIBED event to GUI
self.automatically_added = []
# list of jid to auto-authorize
self._jids_for_auto_auth = set()
def _presence_received(self, _con, stanza, properties):
if properties.from_muc:
# MUC occupant presences are already handled in MUC module
return
muc = self._con.get_module('MUC').get_manager().get(properties.jid)
if muc is not None:
# Presence from the MUC itself, used for MUC avatar
# handled in VCardAvatars module
return
self._log.info('Received from %s', properties.jid)
if properties.type == PresenceType.ERROR:
self._log.info('Error: %s %s', properties.jid, properties.error)
return
if self._account == 'Local':
app.nec.push_incoming_event(
NetworkEvent('raw-pres-received',
conn=self._con,
stanza=stanza))
return
if properties.is_self_presence:
app.nec.push_incoming_event(
NetworkEvent('our-show',
account=self._account,
show=properties.show.value))
return
jid = properties.jid.bare
roster_item = self._con.get_module('Roster').get_item(jid)
if not properties.is_self_bare and roster_item is None:
# Handle only presence from roster contacts
self._log.warning('Unknown presence received')
self._log.warning(stanza)
return
show = properties.show.value
if properties.type.is_unavailable:
show = 'offline'
event_attrs = {
'conn': self._con,
'stanza': stanza,
'prio': properties.priority,
'need_add_in_roster': False,
'popup': False,
'ptype': properties.type.value,
'jid': properties.jid.bare,
'resource': properties.jid.resource,
'id_': properties.id,
'fjid': str(properties.jid),
'timestamp': properties.timestamp,
'avatar_sha': properties.avatar_sha,
'user_nick': properties.nickname,
'idle_time': properties.idle_timestamp,
'show': show,
'new_show': show,
'old_show': 0,
'status': properties.status,
'contact_list': [],
'contact': None,
}
event_ = NetworkEvent('presence-received', **event_attrs)
# TODO: Refactor
self._update_contact(event_, properties)
app.nec.push_incoming_event(event_)
def _update_contact(self, event, properties):
# Note: A similar method also exists in connection_zeroconf
jid = properties.jid.bare
resource = properties.jid.resource
status_strings = ['offline', 'error', 'online', 'chat', 'away',
'xa', 'dnd']
event.new_show = status_strings.index(event.show)
# Update contact
contact_list = app.contacts.get_contacts(self._account, jid)
if not contact_list:
self._log.warning('No contact found')
return
event.contact_list = contact_list
contact = app.contacts.get_contact_strict(self._account,
properties.jid.bare,
properties.jid.resource)
if contact is None:
contact = app.contacts.get_first_contact_from_jid(self._account,
jid)
if contact is None:
self._log.warning('First contact not found')
return
if (self._is_resource_known(contact_list) and
not app.jid_is_transport(jid)):
# Another resource of an existing contact connected
# Add new contact
event.old_show = 0
contact = app.contacts.copy_contact(contact)
contact.resource = resource
app.contacts.add_contact(self._account, contact)
else:
# Convert the initial roster contact to a contact with resource
contact.resource = resource
event.old_show = 0
if contact.show in status_strings:
event.old_show = status_strings.index(contact.show)
event.need_add_in_roster = True
elif contact.show in status_strings:
event.old_show = status_strings.index(contact.show)
# Update contact with presence data
contact.show = event.show
contact.status = properties.status
contact.priority = properties.priority
contact.idle_time = properties.idle_timestamp
event.contact = contact
if not app.jid_is_transport(jid) and len(contact_list) == 1:
# It's not an agent
if event.old_show == 0 and event.new_show > 1:
if not jid in app.newly_added[self._account]:
app.newly_added[self._account].append(jid)
if jid in app.to_be_removed[self._account]:
app.to_be_removed[self._account].remove(jid)
elif event.old_show > 1 and event.new_show == 0 and \
self._con.state.is_available:
if not jid in app.to_be_removed[self._account]:
app.to_be_removed[self._account].append(jid)
if jid in app.newly_added[self._account]:
app.newly_added[self._account].remove(jid)
if app.jid_is_transport(jid):
return
if properties.type.is_unavailable:
# TODO: This causes problems when another
# resource signs off!
self._con.get_module('Bytestream').stop_all_active_file_transfers(
contact)
self._log_presence(properties)
@staticmethod
def _is_resource_known(contact_list):
if len(contact_list) > 1:
return True
if contact_list[0].resource == '':
return False
return contact_list[0].show not in ('not in roster', 'offline')
def _log_presence(self, properties):
if not app.settings.get('log_contact_status_changes'):
return
if not should_log(self._account, properties.jid.bare):
return
show = ShowConstant[properties.show.name]
if properties.type.is_unavailable:
show = ShowConstant.OFFLINE
app.storage.archive.insert_into_logs(self._account,
properties.jid.bare,
time.time(),
KindConstant.STATUS,
message=properties.status,
show=show)
def _subscribe_received(self, _con, _stanza, properties):
jid = properties.jid.bare
fjid = str(properties.jid)
is_transport = app.jid_is_transport(fjid)
auto_auth = app.settings.get_account_setting(self._account, 'autoauth')
self._log.info('Received Subscribe: %s, transport: %s, '
'auto_auth: %s, user_nick: %s',
properties.jid, is_transport,
auto_auth, properties.nickname)
if auto_auth or jid in self._jids_for_auto_auth:
self.send_presence(fjid, 'subscribed')
self._jids_for_auto_auth.discard(jid)
self._log.info('Auto respond with subscribed: %s', jid)
return
status = (properties.status or
_('I would like to add you to my roster.'))
app.nec.push_incoming_event(NetworkEvent(
'subscribe-presence-received',
conn=self._con,
jid=jid,
fjid=fjid,
status=status,
user_nick=properties.nickname,
is_transport=is_transport))
raise nbxmpp.NodeProcessed
def _subscribed_received(self, _con, _stanza, properties):
jid = properties.jid.bare
self._log.info('Received Subscribed: %s', properties.jid)
if jid in self.automatically_added:
self.automatically_added.remove(jid)
raise nbxmpp.NodeProcessed
app.nec.push_incoming_event(NetworkEvent(
'subscribed-presence-received',
account=self._account,
jid=properties.jid))
raise nbxmpp.NodeProcessed
def _unsubscribe_received(self, _con, _stanza, properties):
self._log.info('Received Unsubscribe: %s', properties.jid)
raise nbxmpp.NodeProcessed
def _unsubscribed_received(self, _con, _stanza, properties):
self._log.info('Received Unsubscribed: %s', properties.jid)
app.nec.push_incoming_event(NetworkEvent(
'unsubscribed-presence-received',
conn=self._con, jid=properties.jid.bare))
raise nbxmpp.NodeProcessed
def subscribed(self, jid):
if not app.account_is_available(self._account):
return
self._log.info('Subscribed: %s', jid)
self.send_presence(jid, 'subscribed')
def unsubscribed(self, jid):
if not app.account_is_available(self._account):
return
self._log.info('Unsubscribed: %s', jid)
self._jids_for_auto_auth.discard(jid)
self.send_presence(jid, 'unsubscribed')
def unsubscribe(self, jid, remove_auth=True):
if not app.account_is_available(self._account):
return
if remove_auth:
self._con.get_module('Roster').del_item(jid)
else:
self._log.info('Unsubscribe from %s', jid)
self._jids_for_auto_auth.discard(jid)
self._con.get_module('Roster').unsubscribe(jid)
self._con.get_module('Roster').set_item(jid)
def subscribe(self, jid, msg=None, name='', groups=None, auto_auth=False):
if not app.account_is_available(self._account):
return
if groups is None:
groups = []
self._log.info('Request Subscription to %s', jid)
if auto_auth:
self._jids_for_auto_auth.add(jid)
infos = {'jid': jid}
if name:
infos['name'] = name
iq = nbxmpp.Iq('set', Namespace.ROSTER)
query = iq.setQuery()
item = query.addChild('item', attrs=infos)
for group in groups:
item.addChild('group').setData(group)
self._con.connection.send(iq)
self.send_presence(jid,
'subscribe',
status=msg,
nick=app.nicks[self._account])
def get_presence(self, to=None, typ=None, priority=None,
show=None, status=None, nick=None, caps=True,
idle_time=False):
if show not in ('chat', 'away', 'xa', 'dnd'):
# Gajim sometimes passes invalid show values here
# until this is fixed this is a workaround
show = None
presence = nbxmpp.Presence(to, typ, priority, show, status)
if nick is not None:
nick_tag = presence.setTag('nick', namespace=Namespace.NICK)
nick_tag.setData(nick)
if (idle_time and
app.is_installed('IDLE') and
app.settings.get('autoaway')):
idle_sec = idle.Monitor.get_idle_sec()
time_ = time.strftime('%Y-%m-%dT%H:%M:%SZ',
time.gmtime(time.time() - idle_sec))
idle_node = presence.setTag('idle', namespace=Namespace.IDLE)
idle_node.setAttr('since', time_)
caps = self._con.get_module('Caps').caps
if caps is not None and typ != 'unavailable':
presence.setTag('c',
namespace=Namespace.CAPS,
attrs=caps._asdict())
return presence
def send_presence(self, *args, **kwargs):
if not app.account_is_connected(self._account):
return
presence = self.get_presence(*args, **kwargs)
app.plugin_manager.extension_point(
'send-presence', self._account, presence)
self._log.debug('Send presence:\n%s', presence)
self._con.connection.send(presence)
def get_instance(*args, **kwargs):
return Presence(*args, **kwargs), 'Presence'

View File

@ -0,0 +1,85 @@
# Copyright (C) 2006 Tomasz Melcer <liori AT exroot.org>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0060: Publish-Subscribe
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.modules.base import BaseModule
class PubSub(BaseModule):
_nbxmpp_extends = 'PubSub'
_nbxmpp_methods = [
'publish',
'delete',
'set_node_configuration',
'get_node_configuration',
'get_access_model',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.publish_options = False
def pass_disco(self, info):
if Namespace.PUBSUB_PUBLISH_OPTIONS in info.features:
self._log.info('Discovered Pubsub publish options: %s', info.jid)
self.publish_options = True
def send_pb_subscription_query(self, jid, cb, **kwargs):
if not app.account_is_available(self._account):
return
query = nbxmpp.Iq('get', to=jid)
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
pb.addChild('subscriptions')
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
def send_pb_subscribe(self, jid, node, cb, **kwargs):
if not app.account_is_available(self._account):
return
our_jid = app.get_jid_from_account(self._account)
query = nbxmpp.Iq('set', to=jid)
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
pb.addChild('subscribe', {'node': node, 'jid': our_jid})
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
def send_pb_unsubscribe(self, jid, node, cb, **kwargs):
if not app.account_is_available(self._account):
return
our_jid = app.get_jid_from_account(self._account)
query = nbxmpp.Iq('set', to=jid)
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
pb.addChild('unsubscribe', {'node': node, 'jid': our_jid})
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
def get_instance(*args, **kwargs):
return PubSub(*args, **kwargs), 'PubSub'

View File

@ -0,0 +1,112 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0184: Message Delivery Receipts
import nbxmpp
from nbxmpp.structs import StanzaHandler
from nbxmpp.namespaces import Namespace
from nbxmpp.modules.receipts import build_receipt
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class Receipts(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_receipt,
ns=Namespace.RECEIPTS,
priority=46),
]
def _process_message_receipt(self, _con, stanza, properties):
if not properties.is_receipt:
return
if properties.type.is_error:
if properties.receipt.is_request:
return
# Don't propagate this event further
raise nbxmpp.NodeProcessed
if (properties.type.is_groupchat or
properties.is_self_message or
properties.is_mam_message or
properties.is_carbon_message and properties.carbon.is_sent):
if properties.receipt.is_received:
# Don't propagate this event further
raise nbxmpp.NodeProcessed
return
if properties.receipt.is_request:
if not app.settings.get_account_setting(self._account,
'answer_receipts'):
return
if properties.eme is not None:
# Don't send receipt for message which couldn't be decrypted
if not properties.is_encrypted:
return
contact = self._get_contact(properties)
if contact is None:
return
self._log.info('Send receipt: %s', properties.jid)
self._con.connection.send(build_receipt(stanza))
return
if properties.receipt.is_received:
self._log.info('Receipt from %s %s',
properties.jid,
properties.receipt.id)
jid = properties.jid
if not properties.is_muc_pm:
jid = jid.new_as_bare()
app.storage.archive.set_marker(
app.get_jid_from_account(self._account),
jid,
properties.receipt.id,
'received')
app.nec.push_incoming_event(
NetworkEvent('receipt-received',
account=self._account,
jid=jid,
receipt_id=properties.receipt.id))
raise nbxmpp.NodeProcessed
def _get_contact(self, properties):
if properties.is_muc_pm:
return app.contacts.get_gc_contact(self._account,
properties.jid.bare,
properties.jid.resource)
contact = app.contacts.get_contact(self._account,
properties.jid.bare)
if contact is not None and contact.sub not in ('to', 'none'):
return contact
return None
def get_instance(*args, **kwargs):
return Receipts(*args, **kwargs), 'Receipts'

View File

@ -0,0 +1,44 @@
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0077: In-Band Registration
from nbxmpp.namespaces import Namespace
from gajim.common.modules.base import BaseModule
class Register(BaseModule):
_nbxmpp_extends = 'Register'
_nbxmpp_methods = [
'unregister',
'change_password',
'change_password_with_form',
'request_register_form',
'submit_register_form',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.supported = False
def pass_disco(self, info):
self.supported = Namespace.REGISTER in info.features
def get_instance(*args, **kwargs):
return Register(*args, **kwargs), 'Register'

Some files were not shown because too many files have changed in this diff Show More