9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz
This commit is contained in:
parent
a5b3822651
commit
4c1b226bff
1045 changed files with 753037 additions and 18 deletions
674
COPYING
Normal file
674
COPYING
Normal 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
16
LICENSE
|
@ -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
5
MANIFEST.in
Normal 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
12
PKG-INFO
Normal 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
101
README.md
|
@ -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.
|
||||
|
|
86
data/gajim-history-manager.1
Normal file
86
data/gajim-history-manager.1
Normal 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
119
data/gajim-remote.1
Normal 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
138
data/gajim.1
Normal 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
|
91
data/org.gajim.Gajim.appdata.xml.in
Normal file
91
data/org.gajim.Gajim.appdata.xml.in
Normal 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>
|
28
data/org.gajim.Gajim.desktop.in
Normal file
28
data/org.gajim.Gajim.desktop.in
Normal 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
12
gajim.egg-info/PKG-INFO
Normal 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
1043
gajim.egg-info/SOURCES.txt
Normal file
File diff suppressed because it is too large
Load diff
1
gajim.egg-info/dependency_links.txt
Normal file
1
gajim.egg-info/dependency_links.txt
Normal file
|
@ -0,0 +1 @@
|
|||
|
7
gajim.egg-info/entry_points.txt
Normal file
7
gajim.egg-info/entry_points.txt
Normal 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
|
||||
|
10
gajim.egg-info/requires.txt
Normal file
10
gajim.egg-info/requires.txt
Normal 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
|
1
gajim.egg-info/top_level.txt
Normal file
1
gajim.egg-info/top_level.txt
Normal file
|
@ -0,0 +1 @@
|
|||
gajim
|
19
gajim/__init__.py
Normal file
19
gajim/__init__.py
Normal 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
325
gajim/app_actions.py
Normal 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
562
gajim/application.py
Normal 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
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
1615
gajim/chat_control_base.py
Normal file
File diff suppressed because it is too large
Load diff
20
gajim/command_system/__init__.py
Normal file
20
gajim/command_system/__init__.py
Normal 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.
|
||||
"""
|
135
gajim/command_system/dispatcher.py
Normal file
135
gajim/command_system/dispatcher.py
Normal 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)
|
54
gajim/command_system/errors.py
Normal file
54
gajim/command_system/errors.py
Normal 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.
|
||||
"""
|
351
gajim/command_system/framework.py
Normal file
351
gajim/command_system/framework.py
Normal 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
|
20
gajim/command_system/implementation/__init__.py
Normal file
20
gajim/command_system/implementation/__init__.py
Normal 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.
|
||||
"""
|
131
gajim/command_system/implementation/custom.py
Normal file
131
gajim/command_system/implementation/custom.py
Normal 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."
|
136
gajim/command_system/implementation/execute.py
Normal file
136
gajim/command_system/implementation/execute.py
Normal 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)
|
45
gajim/command_system/implementation/hosts.py
Normal file
45
gajim/command_system/implementation/hosts.py
Normal 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
|
195
gajim/command_system/implementation/middleware.py
Normal file
195
gajim/command_system/implementation/middleware.py
Normal 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()
|
433
gajim/command_system/implementation/standard.py
Normal file
433
gajim/command_system/implementation/standard.py
Normal 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)
|
349
gajim/command_system/mapping.py
Normal file
349
gajim/command_system/mapping.py
Normal 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
|
34
gajim/command_system/tools.py
Normal file
34
gajim/command_system/tools.py
Normal 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
0
gajim/common/__init__.py
Normal file
32
gajim/common/account.py
Normal file
32
gajim/common/account.py
Normal 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
691
gajim/common/app.py
Normal 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()
|
89
gajim/common/cert_store.py
Normal file
89
gajim/common/cert_store.py
Normal 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
642
gajim/common/client.py
Normal 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 don’t 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
643
gajim/common/config.py
Normal 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
234
gajim/common/configpaths.py
Normal 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
201
gajim/common/connection.py
Normal 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)
|
181
gajim/common/connection_handlers.py
Normal file
181
gajim/common/connection_handlers.py
Normal 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)
|
435
gajim/common/connection_handlers_events.py
Normal file
435
gajim/common/connection_handlers_events.py
Normal 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
1115
gajim/common/const.py
Normal file
File diff suppressed because it is too large
Load diff
991
gajim/common/contacts.py
Normal file
991
gajim/common/contacts.py
Normal 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()
|
0
gajim/common/dbus/__init__.py
Normal file
0
gajim/common/dbus/__init__.py
Normal file
112
gajim/common/dbus/location.py
Normal file
112
gajim/common/dbus/location.py
Normal 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
142
gajim/common/dbus/logind.py
Normal 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()
|
208
gajim/common/dbus/music_track.py
Normal file
208
gajim/common/dbus/music_track.py
Normal 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
229
gajim/common/dh.py
Normal 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
464
gajim/common/events.py
Normal 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
157
gajim/common/exceptions.py
Normal 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
173
gajim/common/file_props.py
Normal 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()
|
110
gajim/common/filetransfer.py
Normal file
110
gajim/common/filetransfer.py
Normal 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
110
gajim/common/ged.py
Normal 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
1442
gajim/common/helpers.py
Normal file
File diff suppressed because it is too large
Load diff
194
gajim/common/i18n.py
Normal file
194
gajim/common/i18n.py
Normal 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 don’t 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
362
gajim/common/idle.py
Normal 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()
|
245
gajim/common/jingle_content.py
Normal file
245
gajim/common/jingle_content.py
Normal 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
409
gajim/common/jingle_ft.py
Normal 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
|
228
gajim/common/jingle_ftstates.py
Normal file
228
gajim/common/jingle_ftstates.py
Normal 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
534
gajim/common/jingle_rtp.py
Normal 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=_('Couldn’t 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
|
849
gajim/common/jingle_session.py
Normal file
849
gajim/common/jingle_session.py
Normal 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 don’t 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))
|
502
gajim/common/jingle_transport.py
Normal file
502
gajim/common/jingle_transport.py
Normal 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
282
gajim/common/jingle_xtls.py
Normal 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')
|
231
gajim/common/logging_helpers.py
Normal file
231
gajim/common/logging_helpers.py
Normal 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')
|
174
gajim/common/modules/__init__.py
Normal file
174
gajim/common/modules/__init__.py
Normal 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
|
434
gajim/common/modules/adhoc_commands.py
Normal file
434
gajim/common/modules/adhoc_commands.py
Normal 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'
|
66
gajim/common/modules/annotations.py
Normal file
66
gajim/common/modules/annotations.py
Normal 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'
|
37
gajim/common/modules/announce.py
Normal file
37
gajim/common/modules/announce.py
Normal 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'
|
97
gajim/common/modules/base.py
Normal file
97
gajim/common/modules/base.py
Normal 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, can’t 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, can’t 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()
|
204
gajim/common/modules/bits_of_binary.py
Normal file
204
gajim/common/modules/bits_of_binary.py
Normal 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'
|
139
gajim/common/modules/blocking.py
Normal file
139
gajim/common/modules/blocking.py
Normal 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'
|
353
gajim/common/modules/bookmarks.py
Normal file
353
gajim/common/modules/bookmarks.py
Normal 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'
|
719
gajim/common/modules/bytestream.py
Normal file
719
gajim/common/modules/bytestream.py
Normal 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'
|
232
gajim/common/modules/caps.py
Normal file
232
gajim/common/modules/caps.py
Normal 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'
|
44
gajim/common/modules/carbons.py
Normal file
44
gajim/common/modules/carbons.py
Normal 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'
|
123
gajim/common/modules/chat_markers.py
Normal file
123
gajim/common/modules/chat_markers.py
Normal 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'
|
358
gajim/common/modules/chatstates.py
Normal file
358
gajim/common/modules/chatstates.py
Normal 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:
|
||||
# Don’t 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:
|
||||
# Don’t 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:
|
||||
# Don’t 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'
|
56
gajim/common/modules/delimiter.py
Normal file
56
gajim/common/modules/delimiter.py
Normal 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'
|
265
gajim/common/modules/discovery.py
Normal file
265
gajim/common/modules/discovery.py
Normal 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'
|
116
gajim/common/modules/entity_time.py
Normal file
116
gajim/common/modules/entity_time.py
Normal 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'
|
101
gajim/common/modules/gateway.py
Normal file
101
gajim/common/modules/gateway.py
Normal 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'
|
78
gajim/common/modules/http_auth.py
Normal file
78
gajim/common/modules/http_auth.py
Normal 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'
|
404
gajim/common/modules/httpupload.py
Normal file
404
gajim/common/modules/httpupload.py
Normal 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
239
gajim/common/modules/ibb.py
Normal 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'
|
88
gajim/common/modules/iq.py
Normal file
88
gajim/common/modules/iq.py
Normal 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'
|
311
gajim/common/modules/jingle.py
Normal file
311
gajim/common/modules/jingle.py
Normal 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'
|
59
gajim/common/modules/last_activity.py
Normal file
59
gajim/common/modules/last_activity.py
Normal 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
507
gajim/common/modules/mam.py
Normal 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'
|
390
gajim/common/modules/message.py
Normal file
390
gajim/common/modules/message.py
Normal 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
|
||||
# Don’t 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'
|
107
gajim/common/modules/metacontacts.py
Normal file
107
gajim/common/modules/metacontacts.py
Normal 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'
|
51
gajim/common/modules/misc.py
Normal file
51
gajim/common/modules/misc.py
Normal 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 don’t 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
857
gajim/common/modules/muc.py
Normal 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 don’t 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, don’t 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'
|
39
gajim/common/modules/pep.py
Normal file
39
gajim/common/modules/pep.py
Normal 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'
|
82
gajim/common/modules/ping.py
Normal file
82
gajim/common/modules/ping.py
Normal 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'
|
394
gajim/common/modules/presence.py
Normal file
394
gajim/common/modules/presence.py
Normal 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'
|
85
gajim/common/modules/pubsub.py
Normal file
85
gajim/common/modules/pubsub.py
Normal 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'
|
112
gajim/common/modules/receipts.py
Normal file
112
gajim/common/modules/receipts.py
Normal 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'
|
44
gajim/common/modules/register.py
Normal file
44
gajim/common/modules/register.py
Normal 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
Loading…
Add table
Add a link
Reference in a new issue