Compare commits
302 Commits
Author | SHA1 | Date |
---|---|---|
a3x | bbe65f451a | |
a3x | 5975c3d511 | |
Steffen Vogel | d022243f6c | |
Steffen Vogel | 3ccb164f20 | |
Steffen Vogel | 071f153564 | |
Steffen Vogel | 8d34923ef5 | |
Steffen Vogel | 2ec730020f | |
Steffen Vogel | c79de527ff | |
Steffen Vogel | 750d84bd5c | |
Steffen Vogel | 279fda95c1 | |
Steffen Vogel | 26cf5722b2 | |
Steffen Vogel | 8814e462d3 | |
Steffen Vogel | 3adab4daa0 | |
Steffen Vogel | b93645285c | |
Steffen Vogel | d13c0205c8 | |
Steffen Vogel | 71758d2b7c | |
Steffen Vogel | 06dfe7dc94 | |
Steffen Vogel | 8263d4659a | |
Steffen Vogel | 23b80406da | |
Steffen Vogel | 5c6b4d94fa | |
Vitaly Takmazov | 8d47ac37a9 | |
Steffen Vogel | 35afbe5d4a | |
Steffen Vogel | 61d467b393 | |
Steffen Vogel | 004422e81e | |
Steffen Vogel | 8ae801cf90 | |
Steffen Vogel | 65faf3766a | |
Bee | 6e8bf2b616 | |
Steffen Vogel | 269dbdc12d | |
Steffen Vogel | c359751a44 | |
Steffen Vogel | acceb3618b | |
Steffen Vogel | b4f57753c8 | |
Steffen Vogel | 4244920435 | |
Steffen Vogel | f40102c0c7 | |
Nicolas | 42f8c079c3 | |
Nicolas | d88d13d1e3 | |
Nicolas | 0c199a8231 | |
Nico | 02b45a233e | |
Nico | 2911274ab6 | |
Steffen Vogel | 76fb1b4a6d | |
Bee | 6563c7282b | |
Nico | a51a6b7272 | |
Nico | ce70a912dd | |
Steffen Vogel | fe0ffe7891 | |
Steffen Vogel | b416990d98 | |
rigid | fd477b0eea | |
rigid | c96fbbb049 | |
Steffen Vogel | d85a554e15 | |
Steffen Vogel | e3ebdae54e | |
Nico | 13ac8b4d31 | |
Nico | c3c76e4cb7 | |
Nico | 7eb4707397 | |
Nico | 5caba690cb | |
Steffen Vogel | 085b5a7675 | |
Steffen Vogel | 52ec521ade | |
Steffen Vogel | 9b1e2c1462 | |
Steffen Vogel | 81687c9402 | |
Steffen Vogel | ed2f1444a5 | |
Steffen Vogel | d18b3ab4d9 | |
Steffen Vogel | dd366cb085 | |
Steffen Vogel | 766c40c790 | |
Steffen Vogel | 9b0eb9911a | |
Steffen Vogel | 7729d61d27 | |
Steffen Vogel | 23bc90adf6 | |
Steffen Vogel | ccd866c72f | |
Steffen Vogel | 7187bedafe | |
Steffen Vogel | 64f444e0e6 | |
Steffen Vogel | c1f1ba172b | |
Johannes Wienke | 324acb80fd | |
Johannes Wienke | a90c3ef923 | |
Johannes Wienke | 2562983933 | |
Steffen Vogel | 8ba70e6aea | |
Steffen Vogel | b5cb44170e | |
rigid | 71fef2bf08 | |
rigid | 09e9e62c20 | |
Steffen Vogel | 2bb8ca83fc | |
Steffen Vogel | a1b8cf0ca9 | |
Steffen Vogel | 86b81b60a8 | |
Steffen Vogel | 484b175706 | |
Steffen Vogel | ce2419ba27 | |
Steffen Vogel | 9e4ce9032f | |
Steffen Vogel | fb685e5faf | |
kiv1n | 8e92eaedc2 | |
kiv1n | c12b136b76 | |
kiv1n | 118d65cbd8 | |
git-o-bot | e0d25122bd | |
git-o-bot | 5e1636237e | |
git-o-bot | 5063966772 | |
Steffen Vogel | f2ccbc003b | |
DaZZZl | 838f2bc712 | |
DaZZZl | 49fc66444c | |
DaZZZl | 80059a7b91 | |
Daniel Hiepler | 5b7f8c9646 | |
dazzzl | 7f71b56988 | |
Daniel Hiepler | 0574044472 | |
DaZZZl | 88b43de0f2 | |
DaZZZl | b518fb07af | |
Daniel Hiepler | 60480d02ea | |
Daniel Hiepler | 47dd2a0bdf | |
moyamo | a398a69eaa | |
moyamo | d1e67d0555 | |
Daniel Hiepler | c8d6d95d9e | |
Daniel Hiepler | d1a2cc63d9 | |
Daniel Hiepler | 6107e38a69 | |
Daniel Hiepler | c5249e407d | |
Daniel Hiepler | 5f979e37a9 | |
git-o-bot | c2ea9311fc | |
Daniel Hiepler | f25046de2d | |
moyamo | 875717bf9a | |
moyamo | c72f0e9616 | |
moyamo | ef50b4c1db | |
moyamo | d9bd18013d | |
Steffen Vogel | 22725c042f | |
Steffen Vogel | c005c31afa | |
Steffen Vogel | 381a78eee2 | |
Steffen Vogel | feb4e4ac88 | |
Steffen Vogel | c66d492707 | |
Steffen Vogel | 417465789f | |
Steffen Vogel | 22e5419cda | |
DaZZZl | 26dc1dbf04 | |
Steffen Vogel | 229d7b9e2e | |
DaZZZl | e357cef047 | |
moyamo | 4643bb433d | |
moyamo | f6e2ba94cc | |
Steffen Vogel | dc3033c811 | |
DaZZZl | 2683b774f0 | |
DaZZZl | 4191c2864d | |
Steffen Vogel | 9eae269918 | |
DaZZZl | d6f0f11efc | |
dazzzl | bb49001ab2 | |
Steffen Vogel | 72365712a9 | |
moyamo | 502077dc51 | |
moyamo | 80e7fad07f | |
moyamo | df305b8491 | |
moyamo | 4273153d84 | |
moyamo | 3fba33ac8b | |
moyamo | 2c2c0fde8d | |
moyamo | 8bbdbb970d | |
moyamo | dbf4a477ec | |
moyamo | 026f21f43e | |
moyamo | aeb43dff55 | |
moyamo | 513cdbcf10 | |
moyamo | e850402095 | |
moyamo | 9c32dfbb38 | |
moyamo | f57b276665 | |
moyamo | 59f0af9f8d | |
Mohammed Yaseen Mowzer | 7486028b35 | |
Stefan Müller | 32f6e03b94 | |
Stefan Müller | 41030d5499 | |
moyamo | 16bdc85471 | |
moyamo | 7b327c2d0f | |
moyamo | 8e1be86319 | |
moyamo | 2895f78ec7 | |
moyamo | e32365a065 | |
moyamo | 5cd4442ddc | |
moyamo | 7ff39317cd | |
moyamo | ed6affe1ec | |
Mohammed Yaseen Mowzer | 02d2568663 | |
Stefan Müller | 36eda158f2 | |
moyamo | 2d9ac3037e | |
moyamo | ee743ac327 | |
moyamo | ef04f49152 | |
moyamo | 0b4e2f13d4 | |
moyamo | 03e56a4c07 | |
moyamo | 5fb84bca4f | |
moyamo | 1446e82dc0 | |
moyamo | d4a0593874 | |
moyamo | 22cebef457 | |
moyamo | bed8214a11 | |
moyamo | 3781dcfc1c | |
moyamo | bbf5a08697 | |
moyamo | fae4c648ee | |
moyamo | 4093568234 | |
moyamo | eba8d24a92 | |
moyamo | 02a27ecac8 | |
Mohammed Yaseen Mowzer | c40d477667 | |
moyamo | c1736e4c47 | |
moyamo | 249e8582cb | |
moyamo | 8e9e8a6476 | |
Mohammed Yaseen Mowzer | 034a2adfd3 | |
moyamo | 3c16f39479 | |
moyamo | 40131a06eb | |
moyamo | 9434b1db6f | |
dazzzl | cd44b01e94 | |
moyamo | 7302c4fc2c | |
moyamo | 97c8cfed94 | |
Steffen Vogel | 1af2b5072f | |
moyamo | 87ef2443f3 | |
Steffen Vogel | df773a9670 | |
Steffen Vogel | 7f227e5643 | |
Steffen Vogel | 75a0f310e3 | |
Steffen Vogel | a9e97d16d5 | |
Steffen Vogel | f5a668e589 | |
Steffen Vogel | ce8fd39b35 | |
Steffen Vogel | a84e7258d5 | |
Steffen Vogel | 0594a887dc | |
Steffen Vogel | 10e793fc46 | |
Steffen Vogel | d36c9e0987 | |
Steffen Vogel | 4d07994d02 | |
Steffen Vogel | 927bb0aa5c | |
Steffen Vogel | fa47742682 | |
Steffen Vogel | 759f94ed18 | |
Steffen Vogel | da6ae5fd65 | |
moyamo | dc43506038 | |
moyamo | b78e694531 | |
moyamo | 17c8057db7 | |
moyamo | c44baf892d | |
moyamo | 5a8874c9b9 | |
moyamo | 8385888e97 | |
moyamo | 107354af81 | |
moyamo | da8a415d32 | |
moyamo | 06388685d4 | |
Steffen Vogel | 5c90783bc4 | |
Steffen Vogel | 0421df20a3 | |
Steffen Vogel | b458ef8103 | |
moyamo | e111b0fcff | |
moyamo | b34643a756 | |
moyamo | bd74021ca6 | |
Steffen Vogel | 02e6b89bd1 | |
Steffen Vogel | 246f5cfdb8 | |
moyamo | 8e08efabc9 | |
moyamo | 1cec0a4e30 | |
moyamo | 03d4a98fc7 | |
moyamo | a95b88e7c8 | |
moyamo | cb041395db | |
DaZZZl | f269bf5be7 | |
DaZZZl | bcd5e7e541 | |
root | 40495601ed | |
root | c5b42044b2 | |
root | 98b9487c7f | |
moyamo | dbb66e0381 | |
dazzzl | 1b16e55f73 | |
moyamo | 1c0ae79ec2 | |
Steffen Vogel | 4c86463be8 | |
moyamo | 744efd154f | |
moyamo | a5877bc1a9 | |
moyamo | 29d92b456e | |
moyamo | ecfa2d7f85 | |
moyamo | f1a6b109f7 | |
moyamo | 652a9f0463 | |
moyamo | f4e85f7689 | |
moyamo | 7f1c9cd0db | |
moyamo | 6c12956dd6 | |
moyamo | e92d36a240 | |
Steffen Vogel | 4497b2c5c9 | |
Steffen Vogel | d8140f2dbb | |
Steffen Vogel | f64665b45f | |
Steffen Vogel | 291a7647c0 | |
Steffen Vogel | 54c61f4fcc | |
Steffen Vogel | 1b505f6857 | |
Steffen Vogel | 2496809bf1 | |
moyamo | 646f45434b | |
moyamo | 4fd313e516 | |
moyamo | d2efa0dd7b | |
moyamo | 4c365e3f81 | |
moyamo | a6971ad889 | |
moyamo | e43aeedd9d | |
moyamo | 67c5a7c951 | |
moyamo | 48313c5475 | |
moyamo | 47d52ae22c | |
moyamo | 254465301b | |
moyamo | 8cc3c85145 | |
moyamo | c7afff2cf9 | |
moyamo | 59754848a7 | |
moyamo | 2f73ed1a10 | |
moyamo | f2cadb7492 | |
moyamo | ba54217377 | |
moyamo | f871ed664b | |
moyamo | f0a19d1b6c | |
moyamo | af0150866d | |
moyamo | 85c320e24c | |
moyamo | 8b6a12c798 | |
moyamo | 31bbab2ab1 | |
moyamo | ebed803984 | |
moyamo | 926aa9a6c9 | |
moyamo | 6b1b714d3c | |
moyamo | 0c17a497ee | |
moyamo | ce1bf5d35b | |
moyamo | 68f9ca43ea | |
moyamo | 0fd33d38bb | |
moyamo | e94aa383bf | |
moyamo | 0ecc89d270 | |
moyamo | 83f499b81d | |
moyamo | 8d8711b586 | |
moyamo | f40f077270 | |
moyamo | 97c3aba3ec | |
moyamo | e9f40dbed9 | |
moyamo | 99db6cce76 | |
moyamo | a496e01967 | |
moyamo | c06bfac0a9 | |
moyamo | 467cc80b0d | |
moyamo | 9e35017523 | |
moyamo | b651ba8c37 | |
moyamo | 479efedc26 | |
moyamo | 05525a8a3a | |
moyamo | ae826b1341 | |
moyamo | 74a09ad07f | |
moyamo | 1ebf4e5cc9 | |
moyamo | 9fc0dc7333 | |
moyamo | 1878a30fb6 | |
moyamo | 8dcc028548 | |
moyamo | 886b8ce988 | |
Steffen Vogel | 737d2c55b4 |
674
COPYING.gpl3
674
COPYING.gpl3
|
@ -1,674 +0,0 @@
|
|||
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>.
|
|
@ -0,0 +1,616 @@
|
|||
GNU General Public License
|
||||
==========================
|
||||
|
||||
*Version 3, 29 June 2007*
|
||||
*Copyright © 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>.
|
|
@ -0,0 +1,115 @@
|
|||
Installation and configuration guide
|
||||
====================================
|
||||
|
||||
I assume that you have a basic understanding of XMPP and the the concept
|
||||
of a XMPP component/transport. If not, please get a book about Jabber
|
||||
or read the standards.
|
||||
|
||||
transWhat is a XMPP transport. By this means it extends the
|
||||
functionallity of an existing XMPP server. It acts as a gateway between
|
||||
the XMPP and WhatsApp networks. It receives WhatsApp messages and
|
||||
forwards them to your XMPP client (and vice-versa).
|
||||
|
||||
The implementation of transWhat is based on the `Spectrum 2`_ framework
|
||||
and the `Yowsup 2`_ library to interface with WhatsApp.
|
||||
|
||||
The following chart summarizes the involved components and the protocols
|
||||
they use to communicate.
|
||||
|
||||
Prosody
|
||||
-------
|
||||
|
||||
Installation
|
||||
~~~~~~~~~~~~
|
||||
|
||||
I will not cover the installation of Prosody in this guide. Please look
|
||||
for some other tutorials on how to do that.
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The only important thing for us is the configuration of a XMPP component
|
||||
(Spectrum 2 in our case). See http://prosody.im/doc/components.
|
||||
|
||||
Append the following at the end of ``/etc/prosody/conf.d/transwhat.cfg.lua``
|
||||
|
||||
::
|
||||
|
||||
Component "whatsapp.0l.de"
|
||||
component_secret = "whatsappsucks"
|
||||
component_ports = { 5221 }
|
||||
component_interface = "127.0.0.1"
|
||||
|
||||
Spectrum 2
|
||||
----------
|
||||
|
||||
Installation
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Manual compile latest version from `Github`_. You can use the following
|
||||
guide:
|
||||
http://spectrum.im/documentation/installation/from\_source\_code.html.
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Create a new file ``/etc/spectrum2/transports/transwhat.cfg`` with the
|
||||
following content:
|
||||
|
||||
::
|
||||
|
||||
[service]
|
||||
user = spectrum
|
||||
group = spectrum
|
||||
|
||||
jid = whatsapp.0l.de
|
||||
|
||||
server = localhost
|
||||
password = whatsappsucks
|
||||
port = 5221
|
||||
|
||||
backend_host = localhost
|
||||
backend = /usr/bin/transwhat
|
||||
|
||||
users_per_backend = 10
|
||||
more_resources = 1
|
||||
|
||||
admin_jid = your@jid.example
|
||||
|
||||
[identity]
|
||||
name = transWhat
|
||||
type = xmpp
|
||||
category = gateway
|
||||
|
||||
[logging]
|
||||
config = /etc/spectrum2/logging.cfg
|
||||
backend_config = /etc/spectrum2/backend-logging.cfg
|
||||
|
||||
[database]
|
||||
type = sqlite3
|
||||
|
||||
transWhat
|
||||
---------
|
||||
|
||||
Installation
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Checkout the latest version of transWhat from GitHub:
|
||||
|
||||
::
|
||||
|
||||
$ git clone git@github.com:stv0g/transwhat.git
|
||||
|
||||
Install required dependencies:
|
||||
|
||||
::
|
||||
|
||||
$ pip install --pre protobuf python-dateutil yowsup2
|
||||
|
||||
- yowsup_: is a python library that enables you build application
|
||||
which use WhatsApp service.
|
||||
|
||||
.. _Spectrum 2: http://www.spectrum.im
|
||||
.. _Yowsup 3: https://github.com/tgalal/yowsup
|
||||
.. _Github: https://github.com/hanzz/libtransport
|
||||
.. _yowsup: https://github.com/tgalal/yowsup
|
35
README.md
35
README.md
|
@ -1,35 +0,0 @@
|
|||
# transWhat
|
||||
|
||||
transWhat is a WhatsApp XMPP Gateway based on Spectrum2
|
||||
|
||||
## Dependencies
|
||||
|
||||
#### Spectrum 2
|
||||
is a XMPP transport
|
||||
|
||||
Manual compile latest version from https://github.com/hanzz/libtransport
|
||||
|
||||
#### e4u
|
||||
is a simple emoji4unicode python wrapper library
|
||||
|
||||
Install with `pip install e4u`
|
||||
|
||||
#### Yowsup
|
||||
is a Implementation of the WhatsApp protocol in python
|
||||
|
||||
Use my patched version at https://github.com/stv0g/yowsup
|
||||
|
||||
#### Google Atom and GData Python wrappers
|
||||
required for Google contacts import
|
||||
|
||||
## Contribute
|
||||
|
||||
Pull requests, bug reports etc. are welcome.
|
||||
Help us to provide a open implementation of the WhatsApp protocol.
|
||||
|
||||
## Documentation
|
||||
|
||||
A project wiki is available [here](http://dev.0l.de/projects/transwhat/start).
|
||||
A mailinglist for discussion is available [here](http://lists.0l.de/listinfo/whatsapp).
|
||||
|
||||
A writeup of this project is also availabe at my [blog](http://www.steffenvogel.de/2013/06/29/transwhat/).
|
|
@ -0,0 +1,42 @@
|
|||
transpub
|
||||
=========
|
||||
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
For support and discussions please join the XMPP MUC: **transwhat@conference.0l.de**.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- notifications
|
||||
- Receive data on pubsub extension xep
|
||||
- Set/get online status
|
||||
- Set status message
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
transWhat requires a running and configured XMPP server.
|
||||
Detailed instructions can be found on the `Installation`_ page.
|
||||
|
||||
Users find details on the `Usage`_ page.
|
||||
|
||||
Branches
|
||||
--------
|
||||
|
||||
- `yowsup-3`_ Update to @tgalal’s new Yowsup 3
|
||||
(**recommended**).
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
- An *outdated* project wiki is available `here`_.
|
||||
- An *outdated* writeup of this project is also availabe at my `blog`_.
|
||||
|
||||
.. _Spectrum 2: http://www.spectrum.im
|
||||
.. _Installation: INSTALL.rst
|
||||
.. _Usage: USAGE.rst
|
||||
.. _GPLv3: COPYING.rst
|
|
@ -1,557 +0,0 @@
|
|||
import protocol_pb2
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
import google.protobuf
|
||||
|
||||
def WRAP(MESSAGE, TYPE):
|
||||
wrap = protocol_pb2.WrapperMessage()
|
||||
wrap.type = TYPE
|
||||
wrap.payload = MESSAGE
|
||||
return wrap.SerializeToString()
|
||||
|
||||
class SpectrumBackend:
|
||||
"""
|
||||
Creates new NetworkPlugin and connects the Spectrum2 NetworkPluginServer.
|
||||
@param loop: Event loop.
|
||||
@param host: Host where Spectrum2 NetworkPluginServer runs.
|
||||
@param port: Port.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.m_pingReceived = False
|
||||
self.m_data = ""
|
||||
self.m_init_res = 0
|
||||
|
||||
def handleMessage(self, user, legacyName, msg, nickname = "", xhtml = "", timestamp = ""):
|
||||
m = protocol_pb2.ConversationMessage()
|
||||
m.userName = user
|
||||
m.buddyName = legacyName
|
||||
m.message = msg
|
||||
m.nickname = nickname
|
||||
m.xhtml = xhtml
|
||||
m.timestamp = str(timestamp)
|
||||
|
||||
message = WRAP(m.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_CONV_MESSAGE)
|
||||
self.send(message)
|
||||
|
||||
def handleAttention(self, user, buddyName, msg):
|
||||
m = protocol_pb2.ConversationMessage()
|
||||
m.userName = user
|
||||
m.buddyName = buddyName
|
||||
m.message = msg
|
||||
|
||||
message = WRAP(m.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ATTENTION)
|
||||
self.send(message)
|
||||
|
||||
def handleVCard(self, user, ID, legacyName, fullName, nickname, photo):
|
||||
vcard = protocol_pb2.VCard()
|
||||
vcard.userName = user
|
||||
vcard.buddyName = legacyName
|
||||
vcard.id = ID
|
||||
vcard.fullname = fullName
|
||||
vcard.nickname = nickname
|
||||
vcard.photo = photo
|
||||
|
||||
message = WRAP(vcard.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_VCARD)
|
||||
self.send(message)
|
||||
|
||||
|
||||
def handleSubject(self, user, legacyName, msg, nickname = ""):
|
||||
m = protocol_pb2.ConversationMessage()
|
||||
m.userName = user
|
||||
m.buddyName = legacyName
|
||||
m.message = msg
|
||||
m.nickname = nickname
|
||||
|
||||
message = WRAP(m.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ROOM_SUBJECT_CHANGED)
|
||||
self.send(message)
|
||||
|
||||
def handleBuddyChanged(self, user, buddyName, alias, groups, status, statusMessage = "", iconHash = "", blocked = False):
|
||||
buddy = protocol_pb2.Buddy()
|
||||
buddy.userName = user
|
||||
buddy.buddyName = buddyName
|
||||
buddy.alias = alias
|
||||
buddy.group.extend(groups)
|
||||
buddy.status = status
|
||||
buddy.statusMessage = statusMessage
|
||||
buddy.iconHash = iconHash
|
||||
buddy.blocked = blocked
|
||||
|
||||
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_CHANGED)
|
||||
self.send(message)
|
||||
|
||||
def handleBuddyRemoved(self, user, buddyName):
|
||||
buddy = protocol_pb2.Buddy()
|
||||
buddy.userName = user
|
||||
buddy.buddyName = buddyName
|
||||
|
||||
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_REMOVED)
|
||||
self.send(message);
|
||||
|
||||
def handleBuddyTyping(self, user, buddyName):
|
||||
buddy = protocol_pb2.Buddy()
|
||||
buddy.userName = user
|
||||
buddy.buddyName = buddyName
|
||||
|
||||
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING)
|
||||
self.send(message);
|
||||
|
||||
def handleBuddyTyped(self, user, buddyName):
|
||||
buddy = protocol_pb2.Buddy()
|
||||
buddy.userName = user
|
||||
buddy.buddyName = buddyName
|
||||
|
||||
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED)
|
||||
self.send(message);
|
||||
|
||||
def handleBuddyStoppedTyping(self, user, buddyName):
|
||||
buddy = protocol_pb2.Buddy()
|
||||
buddy.userName = user
|
||||
buddy.buddyName = buddyName
|
||||
|
||||
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING)
|
||||
self.send(message)
|
||||
|
||||
def handleAuthorization(self, user, buddyName):
|
||||
buddy = protocol_pb2.Buddy()
|
||||
buddy.userName = user
|
||||
buddy.buddyName = buddyName
|
||||
|
||||
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_AUTH_REQUEST)
|
||||
self.send(message)
|
||||
|
||||
|
||||
def handleConnected(self, user):
|
||||
d = protocol_pb2.Connected()
|
||||
d.user = user
|
||||
|
||||
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_CONNECTED)
|
||||
self.send(message);
|
||||
|
||||
|
||||
def handleDisconnected(self, user, error = 0, msg = ""):
|
||||
d = protocol_pb2.Disconnected()
|
||||
d.user = user
|
||||
d.error = error
|
||||
d.message = msg
|
||||
|
||||
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_DISCONNECTED)
|
||||
self.send(message);
|
||||
|
||||
|
||||
def handleParticipantChanged(self, user, nickname, room, flags, status, statusMessage = "", newname = ""):
|
||||
d = protocol_pb2.Participant()
|
||||
d.userName = user
|
||||
d.nickname = nickname
|
||||
d.room = room
|
||||
d.flag = flags
|
||||
d.newname = newname
|
||||
d.status = status
|
||||
d.statusMessage = statusMessage
|
||||
|
||||
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_PARTICIPANT_CHANGED)
|
||||
self.send(message);
|
||||
|
||||
|
||||
def handleRoomNicknameChanged(self, user, r, nickname):
|
||||
room = protocol_pb2.Room()
|
||||
room.userName = user
|
||||
room.nickname = nickname
|
||||
room.room = r
|
||||
room.password = ""
|
||||
|
||||
message = WRAP(room.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ROOM_NICKNAME_CHANGED)
|
||||
self.send(message);
|
||||
|
||||
def handleRoomList(self, rooms):
|
||||
roomList = protocol_pb2.RoomList()
|
||||
|
||||
for room in rooms:
|
||||
roomList.room.append(room[0])
|
||||
roomList.name.append(room[1])
|
||||
|
||||
message = WRAP(roomList.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ROOM_LIST)
|
||||
self.send(message);
|
||||
|
||||
|
||||
def handleFTStart(self, user, buddyName, fileName, size):
|
||||
room = protocol_pb2.File()
|
||||
room.userName = user
|
||||
room.buddyName = buddyName
|
||||
room.fileName = fileName
|
||||
room.size = size
|
||||
|
||||
message = WRAP(room.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_START)
|
||||
self.send(message);
|
||||
|
||||
def handleFTFinish(self, user, buddyName, fileName, size, ftid):
|
||||
room = protocol_pb2.File()
|
||||
room.userName = user
|
||||
room.buddyName = buddyName
|
||||
room.fileName = fileName
|
||||
room.size = size
|
||||
|
||||
# Check later
|
||||
if ftid != 0:
|
||||
room.ftID = ftid
|
||||
|
||||
message = WRAP(room.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_FINISH)
|
||||
self.send(message)
|
||||
|
||||
|
||||
def handleFTData(self, ftID, data):
|
||||
d = protocol_pb2.FileTransferData()
|
||||
d.ftid = ftID
|
||||
d.data = data
|
||||
|
||||
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA);
|
||||
self.send(message)
|
||||
|
||||
def handleBackendConfig(self, section, key, value):
|
||||
c = protocol_pb2.BackendConfig()
|
||||
c.config = "[%s]\n%s = %s\n" % (section, key, value)
|
||||
|
||||
message = WRAP(c.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BACKEND_CONFIG);
|
||||
self.send(message)
|
||||
|
||||
def handleLoginPayload(self, data):
|
||||
payload = protocol_pb2.Login()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleLoginRequest(payload.user, payload.legacyName, payload.password, payload.extraFields)
|
||||
|
||||
def handleLogoutPayload(self, data):
|
||||
payload = protocol_pb2.Logout()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleLogoutRequest(payload.user, payload.legacyName)
|
||||
|
||||
def handleStatusChangedPayload(self, data):
|
||||
payload = protocol_pb2.Status()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleStatusChangeRequest(payload.userName, payload.status, payload.statusMessage)
|
||||
|
||||
def handleConvMessagePayload(self, data):
|
||||
payload = protocol_pb2.ConversationMessage()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleMessageSendRequest(payload.userName, payload.buddyName, payload.message, payload.xhtml)
|
||||
|
||||
def handleAttentionPayload(self, data):
|
||||
payload = protocol_pb2.ConversationMessage()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleAttentionRequest(payload.userName, payload.buddyName, payload.message)
|
||||
|
||||
def handleFTStartPayload(self, data):
|
||||
payload = protocol_pb2.File()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleFTStartRequest(payload.userName, payload.buddyName, payload.fileName, payload.size, payload.ftID);
|
||||
|
||||
def handleFTFinishPayload(self, data):
|
||||
payload = protocol_pb2.File()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleFTFinishRequest(payload.userName, payload.buddyName, payload.fileName, payload.size, payload.ftID)
|
||||
|
||||
def handleFTPausePayload(self, data):
|
||||
payload = protocol_pb2.FileTransferData()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleFTPauseRequest(payload.ftID)
|
||||
|
||||
def handleFTContinuePayload(self, data):
|
||||
payload = protocol_pb2.FileTransferData()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleFTContinueRequest(payload.ftID)
|
||||
|
||||
def handleJoinRoomPayload(self, data):
|
||||
payload = protocol_pb2.Room()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleJoinRoomRequest(payload.userName, payload.room, payload.nickname, payload.password)
|
||||
|
||||
def handleLeaveRoomPayload(self, data):
|
||||
payload = protocol_pb2.Room()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
self.handleLeaveRoomRequest(payload.userName, payload.room)
|
||||
|
||||
def handleVCardPayload(self, data):
|
||||
payload = protocol_pb2.VCard()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
if payload.HasField('photo'):
|
||||
self.handleVCardUpdatedRequest(payload.userName, payload.photo, payload.nickname)
|
||||
elif len(payload.buddyName) > 0:
|
||||
self.handleVCardRequest(payload.userName, payload.buddyName, payload.id)
|
||||
|
||||
def handleBuddyChangedPayload(self, data):
|
||||
payload = protocol_pb2.Buddy()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
if payload.HasField('blocked'):
|
||||
self.handleBuddyBlockToggled(payload.userName, payload.buddyName, payload.blocked)
|
||||
else:
|
||||
groups = [g for g in payload.group]
|
||||
self.handleBuddyUpdatedRequest(payload.userName, payload.buddyName, payload.alias, groups);
|
||||
|
||||
def handleBuddyRemovedPayload(self, data):
|
||||
payload = protocol_pb2.Buddy()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
groups = [g for g in payload.group]
|
||||
self.handleBuddyRemovedRequest(payload.userName, payload.buddyName, groups);
|
||||
|
||||
def handleChatStatePayload(self, data, msgType):
|
||||
payload = protocol_pb2.Buddy()
|
||||
if (payload.ParseFromString(data) == False):
|
||||
#TODO: ERROR
|
||||
return
|
||||
if msgType == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING:
|
||||
self.handleTypingRequest(payload.userName, payload.buddyName)
|
||||
elif msgType == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED:
|
||||
self.handleTypedRequest(payload.userName, payload.buddyName)
|
||||
elif msgType == protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING:
|
||||
self.handleStoppedTypingRequest(payload.userName, payload.buddyName)
|
||||
|
||||
|
||||
def handleDataRead(self, data):
|
||||
self.m_data += data
|
||||
while len(self.m_data) != 0:
|
||||
expected_size = 0
|
||||
if (len(self.m_data) >= 4):
|
||||
expected_size = struct.unpack('!I', self.m_data[0:4])[0]
|
||||
if (len(self.m_data) - 4 < expected_size):
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
wrapper = protocol_pb2.WrapperMessage()
|
||||
if (wrapper.ParseFromString(self.m_data[4:]) == False):
|
||||
self.m_data = self.m_data[expected_size+4:]
|
||||
return
|
||||
|
||||
self.m_data = self.m_data[4+expected_size:]
|
||||
|
||||
if wrapper.type == protocol_pb2.WrapperMessage.TYPE_LOGIN:
|
||||
self.handleLoginPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_LOGOUT:
|
||||
self.handleLogoutPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_PING:
|
||||
self.sendPong()
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_CONV_MESSAGE:
|
||||
self.handleConvMessagePayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_JOIN_ROOM:
|
||||
self.handleJoinRoomPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_LEAVE_ROOM:
|
||||
self.handleLeaveRoomPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_VCARD:
|
||||
self.handleVCardPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_CHANGED:
|
||||
self.handleBuddyChangedPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_REMOVED:
|
||||
self.handleBuddyRemovedPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_STATUS_CHANGED:
|
||||
self.handleStatusChangedPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING:
|
||||
self.handleChatStatePayload(wrapper.payload, protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED:
|
||||
self.handleChatStatePayload(wrapper.payload, protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING:
|
||||
self.handleChatStatePayload(wrapper.payload, protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_ATTENTION:
|
||||
self.handleAttentionPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_START:
|
||||
self.handleFTStartPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_FINISH:
|
||||
self.handleFTFinishPayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_PAUSE:
|
||||
self.handleFTPausePayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_CONTINUE:
|
||||
self.handleFTContinuePayload(wrapper.payload)
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_EXIT:
|
||||
self.handleExitRequest()
|
||||
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_RAW_XML:
|
||||
self.handleRawXmlRequest(wrapper.payload)
|
||||
|
||||
def send(self, data):
|
||||
header = struct.pack('!I',len(data))
|
||||
self.sendData(header + data)
|
||||
|
||||
def checkPing(self):
|
||||
if (self.m_pingReceived == False):
|
||||
self.handleExitRequest()
|
||||
self.m_pingReceived = False
|
||||
|
||||
|
||||
def sendPong(self):
|
||||
self.m_pingReceived = True
|
||||
wrap = protocol_pb2.WrapperMessage()
|
||||
wrap.type = protocol_pb2.WrapperMessage.TYPE_PONG
|
||||
message = wrap.SerializeToString()
|
||||
self.send(message)
|
||||
self.sendMemoryUsage()
|
||||
|
||||
|
||||
def sendMemoryUsage(self):
|
||||
stats = protocol_pb2.Stats()
|
||||
|
||||
stats.init_res = self.m_init_res
|
||||
res = 0
|
||||
shared = 0
|
||||
|
||||
e_res, e_shared = self.handleMemoryUsage()
|
||||
|
||||
stats.res = res + e_res
|
||||
stats.shared = shared + e_shared
|
||||
stats.id = str(os.getpid())
|
||||
|
||||
message = WRAP(stats.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_STATS)
|
||||
self.send(message)
|
||||
|
||||
|
||||
def handleLoginRequest(self, user, legacyName, password, extra):
|
||||
"""
|
||||
Called when XMPP user wants to connect legacy network.
|
||||
You should connect him to legacy network and call handleConnected or handleDisconnected function later.
|
||||
@param user: XMPP JID of user for which this event occurs.
|
||||
@param legacyName: Legacy network name of this user used for login.
|
||||
@param password: Legacy network password of this user.
|
||||
"""
|
||||
|
||||
#\msc
|
||||
#NetworkPlugin,YourNetworkPlugin,LegacyNetwork;
|
||||
#NetworkPlugin->YourNetworkPlugin [label="handleLoginRequest(...)", URL="\ref NetworkPlugin::handleLoginRequest()"];
|
||||
#YourNetworkPlugin->LegacyNetwork [label="connect the legacy network"];
|
||||
#--- [label="If password was valid and user is connected and logged in"];
|
||||
#YourNetworkPlugin<-LegacyNetwork [label="connected"];
|
||||
#YourNetworkPlugin->NetworkPlugin [label="handleConnected()", URL="\ref NetworkPlugin::handleConnected()"];
|
||||
#--- [label="else"];
|
||||
#YourNetworkPlugin<-LegacyNetwork [label="disconnected"];
|
||||
#YourNetworkPlugin->NetworkPlugin [label="handleDisconnected()", URL="\ref NetworkPlugin::handleDisconnected()"];
|
||||
#\endmsc
|
||||
|
||||
raise NotImplementedError, "Implement me"
|
||||
|
||||
def handleLogoutRequest(self, user, legacyName):
|
||||
"""
|
||||
Called when XMPP user wants to disconnect legacy network.
|
||||
You should disconnect him from legacy network.
|
||||
@param user: XMPP JID of user for which this event occurs.
|
||||
@param legacyName: Legacy network name of this user used for login.
|
||||
"""
|
||||
|
||||
raise NotImplementedError, "Implement me"
|
||||
|
||||
def handleMessageSendRequest(self, user, legacyName, message, xhtml = ""):
|
||||
"""
|
||||
Called when XMPP user sends message to legacy network.
|
||||
@param user: XMPP JID of user for which this event occurs.
|
||||
@param legacyName: Legacy network name of buddy or room.
|
||||
@param message: Plain text message.
|
||||
@param xhtml: XHTML message.
|
||||
"""
|
||||
|
||||
raise NotImplementedError, "Implement me"
|
||||
|
||||
def handleVCardRequest(self, user, legacyName, ID):
|
||||
""" Called when XMPP user requests VCard of buddy.
|
||||
@param user: XMPP JID of user for which this event occurs.
|
||||
@param legacyName: Legacy network name of buddy whose VCard is requested.
|
||||
@param ID: ID which is associated with this request. You have to pass it to handleVCard function when you receive VCard."""
|
||||
|
||||
#\msc
|
||||
#NetworkPlugin,YourNetworkPlugin,LegacyNetwork;
|
||||
#NetworkPlugin->YourNetworkPlugin [label="handleVCardRequest(...)", URL="\ref NetworkPlugin::handleVCardRequest()"];
|
||||
#YourNetworkPlugin->LegacyNetwork [label="start VCard fetching"];
|
||||
#YourNetworkPlugin<-LegacyNetwork [label="VCard fetched"];
|
||||
#YourNetworkPlugin->NetworkPlugin [label="handleVCard()", URL="\ref NetworkPlugin::handleVCard()"];
|
||||
#\endmsc
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def handleVCardUpdatedRequest(self, user, photo, nickname):
|
||||
"""
|
||||
Called when XMPP user updates his own VCard.
|
||||
You should update the VCard in legacy network too.
|
||||
@param user: XMPP JID of user for which this event occurs.
|
||||
@param photo: Raw photo data.
|
||||
"""
|
||||
pass
|
||||
|
||||
def handleJoinRoomRequest(self, user, room, nickname, pasword):
|
||||
pass
|
||||
|
||||
def handleLeaveRoomRequest(self, user, room):
|
||||
pass
|
||||
|
||||
def handleStatusChangeRequest(self, user, status, statusMessage):
|
||||
pass
|
||||
|
||||
def handleBuddyUpdatedRequest(self, user, buddyName, alias, groups):
|
||||
pass
|
||||
|
||||
def handleBuddyRemovedRequest(self, user, buddyName, groups):
|
||||
pass
|
||||
|
||||
def handleBuddyBlockToggled(self, user, buddyName, blocked):
|
||||
pass
|
||||
|
||||
def handleTypingRequest(self, user, buddyName):
|
||||
pass
|
||||
|
||||
def handleTypedRequest(self, user, buddyName):
|
||||
pass
|
||||
|
||||
def handleStoppedTypingRequest(self, user, buddyName):
|
||||
pass
|
||||
|
||||
def handleAttentionRequest(self, user, buddyName, message):
|
||||
pass
|
||||
|
||||
def handleFTStartRequest(self, user, buddyName, fileName, size, ftID):
|
||||
pass
|
||||
|
||||
def handleFTFinishRequest(self, user, buddyName, fileName, size, ftID):
|
||||
pass
|
||||
|
||||
def handleFTPauseRequest(self, ftID):
|
||||
pass
|
||||
|
||||
def handleFTContinueRequest(self, ftID):
|
||||
pass
|
||||
|
||||
def handleMemoryUsage(self):
|
||||
return (0,0)
|
||||
|
||||
def handleExitRequest(self):
|
||||
sys.exit(1)
|
||||
|
||||
def handleRawXmlRequest(self, xml):
|
||||
pass
|
||||
|
||||
def sendData(self, data):
|
||||
pass
|
|
@ -1,34 +0,0 @@
|
|||
import asyncore, socket
|
||||
|
||||
class IOChannel(asyncore.dispatcher):
|
||||
def __init__(self, host, port, callback):
|
||||
asyncore.dispatcher.__init__(self)
|
||||
|
||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.connect((host, port))
|
||||
|
||||
self.callback = callback
|
||||
self.buffer = ""
|
||||
|
||||
def sendData(self, data):
|
||||
self.buffer += data
|
||||
|
||||
def handle_connect(self):
|
||||
pass
|
||||
|
||||
def handle_close(self):
|
||||
self.close()
|
||||
|
||||
def handle_read(self):
|
||||
data = self.recv(65536)
|
||||
self.callback(data)
|
||||
|
||||
def handle_write(self):
|
||||
sent = self.send(self.buffer)
|
||||
self.buffer = self.buffer[sent:]
|
||||
|
||||
def writable(self):
|
||||
return (len(self.buffer) > 0)
|
||||
|
||||
def readable(self):
|
||||
return True
|
|
@ -1,189 +0,0 @@
|
|||
package pbnetwork;
|
||||
|
||||
enum ConnectionError {
|
||||
CONNECTION_ERROR_NETWORK_ERROR = 0;
|
||||
CONNECTION_ERROR_INVALID_USERNAME = 1;
|
||||
CONNECTION_ERROR_AUTHENTICATION_FAILED = 2;
|
||||
CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE = 3;
|
||||
CONNECTION_ERROR_NO_SSL_SUPPORT = 4;
|
||||
CONNECTION_ERROR_ENCRYPTION_ERROR = 5;
|
||||
CONNECTION_ERROR_NAME_IN_USE = 6;
|
||||
CONNECTION_ERROR_INVALID_SETTINGS = 7;
|
||||
CONNECTION_ERROR_CERT_NOT_PROVIDED = 8;
|
||||
CONNECTION_ERROR_CERT_UNTRUSTED = 9;
|
||||
CONNECTION_ERROR_CERT_EXPIRED = 10;
|
||||
CONNECTION_ERROR_CERT_NOT_ACTIVATED = 11;
|
||||
CONNECTION_ERROR_CERT_HOSTNAME_MISMATCH = 12;
|
||||
CONNECTION_ERROR_CERT_FINGERPRINT_MISMATCH = 13;
|
||||
CONNECTION_ERROR_CERT_SELF_SIGNED = 14;
|
||||
CONNECTION_ERROR_CERT_OTHER_ERROR = 15;
|
||||
CONNECTION_ERROR_OTHER_ERROR = 16;
|
||||
}
|
||||
|
||||
enum StatusType {
|
||||
STATUS_ONLINE = 0;
|
||||
STATUS_AWAY = 1;
|
||||
STATUS_FFC = 2;
|
||||
STATUS_XA = 3;
|
||||
STATUS_DND = 4;
|
||||
STATUS_NONE = 5;
|
||||
STATUS_INVISIBLE = 6;
|
||||
}
|
||||
|
||||
message Connected {
|
||||
required string user = 1;
|
||||
}
|
||||
|
||||
message Disconnected {
|
||||
required string user = 1;
|
||||
required int32 error = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
message Login {
|
||||
required string user = 1;
|
||||
required string legacyName = 2;
|
||||
required string password = 3;
|
||||
repeated string extraFields = 4;
|
||||
}
|
||||
|
||||
message Logout {
|
||||
required string user = 1;
|
||||
required string legacyName = 2;
|
||||
}
|
||||
|
||||
message Buddy {
|
||||
required string userName = 1;
|
||||
required string buddyName = 2;
|
||||
optional string alias = 3;
|
||||
repeated string group = 4;
|
||||
optional StatusType status = 5;
|
||||
optional string statusMessage = 6;
|
||||
optional string iconHash = 7;
|
||||
optional bool blocked = 8;
|
||||
}
|
||||
|
||||
message ConversationMessage {
|
||||
required string userName = 1;
|
||||
required string buddyName = 2;
|
||||
required string message = 3;
|
||||
optional string nickname = 4;
|
||||
optional string xhtml = 5;
|
||||
optional string timestamp = 6;
|
||||
optional bool headline = 7;
|
||||
optional string id = 8;
|
||||
optional bool pm = 9;
|
||||
}
|
||||
|
||||
message Room {
|
||||
required string userName = 1;
|
||||
required string nickname = 2;
|
||||
required string room = 3;
|
||||
optional string password = 4;
|
||||
}
|
||||
|
||||
message RoomList {
|
||||
repeated string room = 1;
|
||||
repeated string name = 2;
|
||||
}
|
||||
|
||||
enum ParticipantFlag {
|
||||
PARTICIPANT_FLAG_NONE = 0;
|
||||
PARTICIPANT_FLAG_MODERATOR = 1;
|
||||
PARTICIPANT_FLAG_CONFLICT = 2;
|
||||
PARTICIPANT_FLAG_BANNED = 4;
|
||||
PARTICIPANT_FLAG_NOT_AUTHORIZED = 8;
|
||||
PARTICIPANT_FLAG_ME = 16;
|
||||
PARTICIPANT_FLAG_KICKED = 32;
|
||||
PARTICIPANT_FLAG_ROOM_NOT_FOUND = 64;
|
||||
}
|
||||
|
||||
message Participant {
|
||||
required string userName = 1;
|
||||
required string room = 2;
|
||||
required string nickname = 3;
|
||||
required int32 flag = 4;
|
||||
required StatusType status = 5;
|
||||
optional string statusMessage = 6;
|
||||
optional string newname = 7;
|
||||
}
|
||||
|
||||
message VCard {
|
||||
required string userName = 1;
|
||||
required string buddyName = 2;
|
||||
required int32 id = 3;
|
||||
optional string fullname = 4;
|
||||
optional string nickname = 5;
|
||||
optional bytes photo = 6;
|
||||
}
|
||||
|
||||
message Status {
|
||||
required string userName = 1;
|
||||
required StatusType status = 3;
|
||||
optional string statusMessage = 4;
|
||||
}
|
||||
|
||||
message Stats {
|
||||
required int32 res = 1;
|
||||
required int32 init_res = 2;
|
||||
required int32 shared = 3;
|
||||
required string id = 4;
|
||||
}
|
||||
|
||||
message File {
|
||||
required string userName = 1;
|
||||
required string buddyName = 2;
|
||||
required string fileName = 3;
|
||||
required int32 size = 4;
|
||||
optional int32 ftID = 5;
|
||||
}
|
||||
|
||||
message FileTransferData {
|
||||
required int32 ftID = 1;
|
||||
required bytes data = 2;
|
||||
}
|
||||
|
||||
message BackendConfig {
|
||||
required string config = 1;
|
||||
}
|
||||
|
||||
message WrapperMessage {
|
||||
enum Type {
|
||||
TYPE_CONNECTED = 1;
|
||||
TYPE_DISCONNECTED = 2;
|
||||
TYPE_LOGIN = 3;
|
||||
TYPE_LOGOUT = 4;
|
||||
TYPE_BUDDY_CHANGED = 6;
|
||||
TYPE_BUDDY_REMOVED = 7;
|
||||
TYPE_CONV_MESSAGE = 8;
|
||||
TYPE_PING = 9;
|
||||
TYPE_PONG = 10;
|
||||
TYPE_JOIN_ROOM = 11;
|
||||
TYPE_LEAVE_ROOM = 12;
|
||||
TYPE_PARTICIPANT_CHANGED = 13;
|
||||
TYPE_ROOM_NICKNAME_CHANGED = 14;
|
||||
TYPE_ROOM_SUBJECT_CHANGED = 15;
|
||||
TYPE_VCARD = 16;
|
||||
TYPE_STATUS_CHANGED = 17;
|
||||
TYPE_BUDDY_TYPING = 18;
|
||||
TYPE_BUDDY_STOPPED_TYPING = 19;
|
||||
TYPE_BUDDY_TYPED = 20;
|
||||
TYPE_AUTH_REQUEST = 21;
|
||||
TYPE_ATTENTION = 22;
|
||||
TYPE_STATS = 23;
|
||||
TYPE_FT_START = 24;
|
||||
TYPE_FT_FINISH = 25;
|
||||
TYPE_FT_DATA = 26;
|
||||
TYPE_FT_PAUSE = 27;
|
||||
TYPE_FT_CONTINUE = 28;
|
||||
TYPE_EXIT = 29;
|
||||
TYPE_BACKEND_CONFIG = 30;
|
||||
TYPE_QUERY = 31;
|
||||
TYPE_ROOM_LIST = 32;
|
||||
TYPE_CONV_MESSAGE_ACK = 33;
|
||||
TYPE_RAW_XML = 34;
|
||||
}
|
||||
required Type type = 1;
|
||||
optional bytes payload = 2;
|
||||
}
|
||||
;
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,108 @@
|
|||
Usage
|
||||
=====
|
||||
|
||||
Starting/stopping service
|
||||
-------------------------
|
||||
|
||||
The ``transwhat.py`` script gets started as a backend by Spectrum. You
|
||||
should not try to run it manually. To simplify the management of
|
||||
multiple transports (WhatsApp, IRC, Facebook, libpurple, …), Spectrum
|
||||
provides a little helper tool called ``spectrum_manager``:
|
||||
|
||||
To start transwhat run
|
||||
|
||||
::
|
||||
|
||||
spectrum_manager start whatsapp
|
||||
|
||||
To stop transwhat run
|
||||
|
||||
::
|
||||
|
||||
spectrum_manager stop whatsapp
|
||||
|
||||
Bot
|
||||
---
|
||||
|
||||
You might want to talk to bot if you’re feeling lonely ;-P
|
||||
|
||||
The bot is one of the contacts every user has in its contact list. It
|
||||
offers you a simple way to interact with the server:
|
||||
|
||||
+------------------+--------------------------------------------+
|
||||
| **Command** | **Description** |
|
||||
+==================+============================================+
|
||||
| ``\help`` | show this message |
|
||||
+------------------+--------------------------------------------+
|
||||
| ``\prune`` | clear your buddylist |
|
||||
+------------------+--------------------------------------------+
|
||||
| ``\lastseen`` | request last online timestamp from buddy |
|
||||
+------------------+--------------------------------------------+
|
||||
| ``\leave`` | permanently leave group chat |
|
||||
+------------------+--------------------------------------------+
|
||||
| ``\groups`` | print all attended groups |
|
||||
+------------------+--------------------------------------------+
|
||||
| ``\getgroups`` | get current groups from WA |
|
||||
+------------------+--------------------------------------------+
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<note tip>
|
||||
|
||||
All commands start with a **back**\ slash!
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</note>
|
||||
|
||||
Login
|
||||
-----
|
||||
|
||||
To login to the transWhat, you should use the service discovery option
|
||||
in your XMPP client.
|
||||
|
||||
When asked about the login credentials, enter your data as described
|
||||
below:
|
||||
|
||||
+---------------+-----------------------------+----------------------+
|
||||
| **Setting** | **Value** | **Example** |
|
||||
+===============+=============================+======================+
|
||||
| User | CountryCode + PhoneNumber | 4917634911387 |
|
||||
+---------------+-----------------------------+----------------------+
|
||||
| Password | WhatsApp password | `*Base64 string*`_ |
|
||||
+---------------+-----------------------------+----------------------+
|
||||
|
||||
Buddies
|
||||
~~~~~~~
|
||||
|
||||
WhatsApp does not store your contacts on their servers. Thus you need to
|
||||
import your contacts manually with your XMPP Client or use [[.:bot\|our
|
||||
bot]] to Import your contacts from Google (preferred).
|
||||
|
||||
(In Pidgin: Menu => Buddys => Add Buddy)
|
||||
|
||||
Just use the same JID format as for your login:
|
||||
|
||||
CountryCode + PhoneNumber + “@whatsapp.example.org”
|
||||
|
||||
Groups
|
||||
~~~~~~
|
||||
|
||||
To chat with groups you need to add them manually to your XMPP client.
|
||||
|
||||
To get a list of your WhatsApp groups, you can use the Auto Discovery
|
||||
functionality of your XMPP client.
|
||||
|
||||
(In Pidgin: Menu => Buddys => Join Chat => RoomList)
|
||||
|
||||
Smileys / Emojis
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
To be able to see smileys, you will need an `Unicode Emoji font`_.
|
||||
|
||||
When using Pidgin, you might want to check out my `stv0g’s Unicode emoji
|
||||
theme`_].
|
||||
|
||||
.. _*Base64 string*: https://github.com/davidgfnet/whatsapp-purple#how-do-i-get-my-user-name-and-password
|
||||
.. _Unicode Emoji font: https://github.com/stv0g/unicode-emoji/raw/master/symbola/Symbola.ttf
|
||||
.. _stv0g’s Unicode emoji theme: https://github.com/stv0g/unicode-emoji
|
195
bot.py
195
bot.py
|
@ -1,195 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import inspect
|
||||
import re
|
||||
import urllib
|
||||
import time
|
||||
import os
|
||||
import utils
|
||||
|
||||
from constants import *
|
||||
from googleclient import GoogleClient
|
||||
|
||||
from Yowsup.Contacts.contacts import WAContactsSyncRequest
|
||||
|
||||
class Bot():
|
||||
def __init__(self, session, name = "Bot"):
|
||||
self.session = session
|
||||
self.name = name
|
||||
|
||||
self.google = GoogleClient()
|
||||
|
||||
self.commands = {
|
||||
"import": self._import,
|
||||
"help": self._help,
|
||||
"prune": self._prune,
|
||||
"welcome": self._welcome,
|
||||
"fortune": self._fortune,
|
||||
"sync": self._sync
|
||||
}
|
||||
|
||||
def parse(self, message):
|
||||
args = message.split(" ")
|
||||
cmd = args.pop(0)
|
||||
|
||||
if cmd[0] == '\\':
|
||||
try:
|
||||
self.call(cmd[1:], args)
|
||||
except KeyError:
|
||||
self.send("invalid command")
|
||||
except TypeError:
|
||||
self.send("invalid syntax")
|
||||
else:
|
||||
self.send("a valid command starts with a backslash")
|
||||
|
||||
def call(self, cmd, args = []):
|
||||
func = self.commands[cmd]
|
||||
spec = inspect.getargspec(func)
|
||||
maxs = len(spec.args) - 1
|
||||
reqs = maxs - len(spec.defaults or [])
|
||||
if reqs > len(args) > maxs:
|
||||
raise TypeError()
|
||||
|
||||
thread = threading.Thread(target=func, args=tuple(args))
|
||||
thread.start()
|
||||
|
||||
def send(self, message):
|
||||
self.session.backend.handleMessage(self.session.user, self.name, message)
|
||||
|
||||
def __do_import(self, token):
|
||||
# Google
|
||||
google = self.google.getContacts(token)
|
||||
self.send("%d buddies imported from google" % len(google))
|
||||
|
||||
result = { }
|
||||
for number, name in google.iteritems():
|
||||
number = re.sub("[^0-9]", "", number)
|
||||
number = number if number[0] == "0" else "+" + number
|
||||
|
||||
result[number] = { 'nick': name, 'state': 0 }
|
||||
|
||||
# WhatsApp
|
||||
user = self.session.legacyName
|
||||
password = self.session.password
|
||||
sync = WAContactsSyncRequest(user, password, result.keys())
|
||||
whatsapp = sync.send()['c']
|
||||
|
||||
for w in whatsapp:
|
||||
result[w['p']]['state'] = w['w']
|
||||
result[w['p']]['number'] = w['n']
|
||||
|
||||
self.send("%d buddies are using whatsapp" % len(filter(lambda w: w['w'], whatsapp)))
|
||||
|
||||
for r in result.values():
|
||||
if r['nick']:
|
||||
self.session.buddies.add(
|
||||
number = r['number'],
|
||||
nick = r['nick'],
|
||||
groups = [u'Google'],
|
||||
state = r['state']
|
||||
)
|
||||
|
||||
self.send("%d buddies imported" % len(whatsapp))
|
||||
|
||||
def __get_token(self, filename, timeout = 30):
|
||||
file = open(filename, 'r')
|
||||
file.seek(-1, 2) # look at the end
|
||||
|
||||
count = 0
|
||||
while count < timeout:
|
||||
line = file.readline()
|
||||
|
||||
if line in ["", "\n"]:
|
||||
time.sleep(1)
|
||||
count += 1
|
||||
continue
|
||||
else:
|
||||
timestamp, number, token = line[:-1].split("\t")
|
||||
if (number == self.session.legacyName):
|
||||
file.close()
|
||||
return token
|
||||
|
||||
file.close()
|
||||
|
||||
# commands
|
||||
def _import(self, token = None):
|
||||
if not token:
|
||||
token_url = self.google.getTokenUrl("http://whatsapp.0l.de/auth.py")
|
||||
auth_url = "http://whatsapp.0l.de/auth.py?number=%s&auth_url=%s" % (self.session.legacyName, urllib.quote(token_url))
|
||||
short_url = utils.shorten(auth_url)
|
||||
self.send("please visit this url to auth: %s" % short_url)
|
||||
|
||||
self.send("waiting for authorization...")
|
||||
token = self.__get_token(TOKEN_FILE)
|
||||
if token:
|
||||
self.send("got token: %s" % token)
|
||||
self.__do_import(token)
|
||||
self.session.updateRoster()
|
||||
else:
|
||||
self.send("timeout! please use \"\\import [token]\"")
|
||||
else:
|
||||
self.__do_import(token)
|
||||
self.session.updateRoster()
|
||||
|
||||
def _sync(self):
|
||||
user = self.session.legacyName
|
||||
password = self.session.password
|
||||
|
||||
count = self.session.buddies.sync(user, password)
|
||||
self.session.updateRoster()
|
||||
|
||||
if count:
|
||||
self.send("sync complete, %d buddies are using WhatsApp" % count)
|
||||
else:
|
||||
self.send("sync failed, sorry something went wrong")
|
||||
|
||||
def _help(self):
|
||||
self.send("""following bot commands are available:
|
||||
\\help show this message
|
||||
\\prune clear your buddylist
|
||||
\\import [token] import buddies from Google
|
||||
\\sync sync your imported contacts with WhatsApp
|
||||
\\fortune [database] give me a quote
|
||||
|
||||
following user commands are available:
|
||||
\\lastseen request last online timestamp from buddy""")
|
||||
|
||||
def _fortune(self, database = '', prefix=''):
|
||||
if os.path.exists("/usr/share/games/fortunes/%s" % database):
|
||||
fortune = os.popen('/usr/games/fortune %s' % database).read()
|
||||
self.send(prefix + fortune[:-1])
|
||||
else:
|
||||
self.send("invalid database")
|
||||
|
||||
def _welcome(self):
|
||||
motd = open(MOTD_FILE, "r").read()
|
||||
self.send(motd[:-1])
|
||||
self.call("fortune", ("disclaimer", "Disclaimer: "))
|
||||
|
||||
def _prune(self):
|
||||
self.session.buddies.prune()
|
||||
self.session.updateRoster()
|
||||
self.send("buddy list cleared")
|
164
buddy.py
164
buddy.py
|
@ -1,164 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from Spectrum2 import protocol_pb2
|
||||
from Yowsup.Contacts.contacts import WAContactsSyncRequest
|
||||
|
||||
import logging
|
||||
|
||||
class Number():
|
||||
|
||||
def __init__(self, number, state, db):
|
||||
self.number = number
|
||||
self.db = db
|
||||
self.state = state
|
||||
|
||||
cur = self.db.cursor()
|
||||
cur.execute("SELECT id FROM numbers WHERE number = %s AND state = %s", (self.number, self.state))
|
||||
if (cur.rowcount):
|
||||
self.id = cur.fetchone()[0]
|
||||
else:
|
||||
cur.execute("REPLACE numbers (number, state) VALUES (%s, %s)", (self.number, self.state))
|
||||
self.db.commit()
|
||||
self.id = cur.lastrowid
|
||||
|
||||
def __str__(self):
|
||||
return "%s (id=%s)" % (self.number, self.id)
|
||||
|
||||
|
||||
class Buddy():
|
||||
def __init__(self, owner, number, nick, groups, id, db):
|
||||
self.id = id
|
||||
self.db = db
|
||||
|
||||
self.nick = nick
|
||||
self.owner = owner
|
||||
self.number = number
|
||||
self.groups = groups
|
||||
|
||||
def update(self, nick, groups):
|
||||
self.nick = nick
|
||||
self.groups = groups
|
||||
|
||||
groups = u",".join(groups).encode("latin-1")
|
||||
cur = self.db.cursor()
|
||||
cur.execute("UPDATE buddies SET nick = %s, groups = %s WHERE owner_id = %s AND buddy_id = %s", (self.nick, groups, self.owner.id, self.number.id))
|
||||
self.db.commit()
|
||||
|
||||
def delete(self):
|
||||
cur = self.db.cursor()
|
||||
cur.execute("DELETE FROM buddies WHERE owner_id = %s AND buddy_id = %s", (self.owner.id, self.number.id))
|
||||
self.db.commit()
|
||||
self.id = None
|
||||
|
||||
@staticmethod
|
||||
def create(owner, number, nick, groups, db):
|
||||
groups = u",".join(groups).encode("latin-1")
|
||||
cur = db.cursor()
|
||||
cur.execute("REPLACE buddies (owner_id, buddy_id, nick, groups) VALUES (%s, %s, %s, %s)", (owner.id, number.id, nick, groups))
|
||||
db.commit()
|
||||
|
||||
return Buddy(owner, number, nick, groups, cur.lastrowid, db)
|
||||
|
||||
def __str__(self):
|
||||
return "%s (nick=%s, id=%s)" % (self.number, self.nick, self.id)
|
||||
|
||||
class BuddyList(dict):
|
||||
|
||||
def __init__(self, owner, db):
|
||||
self.db = db
|
||||
self.owner = Number(owner, 1, db)
|
||||
|
||||
def load(self):
|
||||
self.clear()
|
||||
|
||||
cur = self.db.cursor()
|
||||
cur.execute("""SELECT
|
||||
b.id AS id,
|
||||
n.number AS number,
|
||||
b.nick AS nick,
|
||||
b.groups AS groups,
|
||||
n.state AS state
|
||||
FROM buddies AS b
|
||||
LEFT JOIN numbers AS n
|
||||
ON b.buddy_id = n.id
|
||||
WHERE
|
||||
b.owner_id IN (%s, 0)
|
||||
AND n.state >= 1
|
||||
ORDER BY b.owner_id DESC""", self.owner.id)
|
||||
|
||||
for i in range(cur.rowcount):
|
||||
id, number, nick, groups, state = cur.fetchone()
|
||||
self[number] = Buddy(self.owner, Number(number, state, self.db), nick.decode('latin1'), groups.split(","), id, self.db)
|
||||
|
||||
def update(self, number, nick, groups):
|
||||
if number in self:
|
||||
buddy = self[number]
|
||||
buddy.update(nick, groups)
|
||||
else:
|
||||
buddy = self.add(number, nick, groups, 1)
|
||||
|
||||
return buddy
|
||||
|
||||
def add(self, number, nick, groups = [], state = 0):
|
||||
return Buddy.create(self.owner, Number(number, state, self.db), nick, groups, self.db)
|
||||
|
||||
def remove(self, number):
|
||||
buddy = self[number]
|
||||
buddy.delete()
|
||||
|
||||
return buddy
|
||||
|
||||
def prune(self):
|
||||
cur = self.db.cursor()
|
||||
cur.execute("DELETE FROM buddies WHERE owner_id = %s", self.owner.id)
|
||||
self.db.commit()
|
||||
|
||||
def sync(self, user, password):
|
||||
cur = self.db.cursor()
|
||||
cur.execute("""SELECT
|
||||
n.number AS number,
|
||||
n.state AS state
|
||||
FROM buddies AS r
|
||||
LEFT JOIN numbers AS n
|
||||
ON r.buddy_id = n.id
|
||||
WHERE
|
||||
r.owner_id = %s""", self.owner.id)
|
||||
|
||||
# prefix every number with leading 0 to force internation format
|
||||
numbers = dict([("+" + number, state) for number, state in cur.fetchall()])
|
||||
|
||||
if len(numbers) == 0:
|
||||
return 0
|
||||
|
||||
result = WAContactsSyncRequest(user, password, numbers.keys()).send()
|
||||
|
||||
using = 0
|
||||
for number in result['c']:
|
||||
cur = self.db.cursor()
|
||||
cur.execute("UPDATE numbers SET state = %s WHERE number = %s", (number['w'], number['n']))
|
||||
self.db.commit()
|
||||
using += number['w']
|
||||
|
||||
return using
|
73
cgi/auth.py
73
cgi/auth.py
|
@ -1,73 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import cgi
|
||||
import cgitb
|
||||
import time
|
||||
|
||||
sys.path.insert(1, os.path.join(sys.path[0], '..'))
|
||||
|
||||
from constants import *
|
||||
|
||||
def cookies(str):
|
||||
return dict(c.split('=') for c in str.split(";"))
|
||||
|
||||
def save_token(timestamp, number, token, filename="tokens"):
|
||||
file = open(filename, 'a')
|
||||
file.write("%s\t%s\t%s\n" % (str(timestamp), number, token))
|
||||
file.close()
|
||||
|
||||
def main():
|
||||
form = cgi.FieldStorage()
|
||||
number = form.getfirst("number")
|
||||
auth_url = form.getfirst("auth_url")
|
||||
token = form.getfirst("code")
|
||||
|
||||
if auth_url:
|
||||
print "Status: 301 Moved"
|
||||
print "Location: %s" % auth_url
|
||||
print "Content-type: text/html"
|
||||
print "Set-Cookie: number=%s" % number
|
||||
print "\n\n";
|
||||
|
||||
elif token and os.environ.has_key('HTTP_COOKIE'):
|
||||
print "Status: 301 Moved"
|
||||
print "Content-type: text/html"
|
||||
print "Location: http://whatsapp.0l.de"
|
||||
print
|
||||
|
||||
c = cookies(os.environ['HTTP_COOKIE'])
|
||||
save_token(time.time(), c['number'], token, TOKEN_FILE)
|
||||
|
||||
else:
|
||||
print "Content-type: text/html"
|
||||
print "\n"
|
||||
print "something strange happened :("
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;url=http://dev.0l.de/projects/transwhat/start">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
237
cgi/sipgate.py
237
cgi/sipgate.py
|
@ -1,237 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- encoding: UTF8 -*-
|
||||
|
||||
# author: Philipp Klaus, philipp.klaus →AT→ gmail.com
|
||||
|
||||
# This file is part of python-sipgate-xmlrpc.
|
||||
#
|
||||
# python-sipgate-xmlrpc 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.
|
||||
#
|
||||
# python-sipgate-xmlrpc 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 python-sipgate-xmlrpc. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#####################################################################
|
||||
###### This the most important file of the project: #######
|
||||
###### It contains the classe api, which #######
|
||||
###### implements the XML-RPC communication with the #######
|
||||
###### Sipgate API. #######
|
||||
|
||||
#from time import time
|
||||
from sys import stderr
|
||||
from xmlrpclib import ServerProxy, Fault, ProtocolError, ResponseError
|
||||
from exceptions import TypeError
|
||||
from socket import error as socket_error
|
||||
import re
|
||||
|
||||
VERSION = "0.9.2"
|
||||
NAME = "%s - python-sipgate-xmlrpc/sipgate.py"
|
||||
VENDOR = "https://github.com/pklaus/python-sipgate-xmlrpc"
|
||||
|
||||
### ------- Here comes the most important piece of code: the api class with magic methods -----
|
||||
|
||||
class api (ServerProxy):
|
||||
def __init__ (self, username=False, password=False, prog_name=False, verbose=False):
|
||||
if not (username and password and prog_name):
|
||||
raise SipgateAPIException('To use the class sipgate.api you must provide, username, password and a program name.')
|
||||
address = SIPGATE_API_URL % {'username':username, 'password':password}
|
||||
### The super() call would be more modern but it doesn't work with the current Python version yet.
|
||||
#super(api, self).__init__(address, verbose=debug)
|
||||
ServerProxy.__init__(self, address,verbose=verbose)
|
||||
### It is considered good practice to Identify the client talking to the server:
|
||||
self.ClientIdentify({ "ClientName" : NAME % prog_name, "ClientVersion" : VERSION, "ClientVendor" : VENDOR })
|
||||
|
||||
def __getattr__(self,name):
|
||||
return _Method(self.__request, name)
|
||||
|
||||
def __request (self, methodname, params):
|
||||
if methodname.replace(API_PREFIX,'') not in VALID_METHODS:
|
||||
stderr.write( UNKNOWN_METHOD_MESSAGE % {
|
||||
'method': methodname.replace(API_PREFIX,''), 'api_prefix': API_PREFIX,
|
||||
'api_version': SIPGATE_API_DOC_V, 'api_date': SIPGATE_API_DOC_D } )
|
||||
if len(params)>0 and not type(params[0]) is dict:
|
||||
raise TypeError(DICT_AS_PARAM_MESSAGE % methodname.replace(API_PREFIX,''))
|
||||
method_function = ServerProxy.__getattr__(self,methodname)
|
||||
try:
|
||||
result = method_function(params[0] if len(params)>0 and type(params[0]) is dict else dict())
|
||||
# cast the result dictionary to a SipgateResponse (custom dictionary):
|
||||
result = SipgateResponse(result)
|
||||
except Fault, e:
|
||||
raise SipgateAPIFault(e.faultCode, e.faultString)
|
||||
except ProtocolError, e:
|
||||
raise SipgateAPIProtocolError(e.url, e.errcode, e.errmsg, e.headers)
|
||||
except socket_error, (value,message):
|
||||
raise SipgateAPISocketError(value, message)
|
||||
return result
|
||||
|
||||
## <http://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict-and-override-get-set>
|
||||
class SipgateResponse(dict):
|
||||
def __init__(self, response_dict):
|
||||
try:
|
||||
self.StatusCode, self.StatusString = int(response_dict['StatusCode']), response_dict['StatusString']
|
||||
self.success = self.StatusCode == 200
|
||||
except:
|
||||
raise TypeError(RESPONSE_NOT_A_DICTIONARY % response_dict)
|
||||
dict.__init__(self, response_dict)
|
||||
|
||||
class _Method:
|
||||
# With the help of this class the api class does not
|
||||
# need to state explicitly the possible XML-RPC calls.
|
||||
def __init__(self, send, name):
|
||||
self.__send = send
|
||||
self.__name = API_PREFIX+name
|
||||
def __call__(self, *args):
|
||||
return self.__send(self.__name, args)
|
||||
|
||||
### ------ now we define the exceptions that could occur ------
|
||||
|
||||
class SipgateAPIException(Exception):
|
||||
pass
|
||||
|
||||
class SipgateAPIFault(Fault, SipgateAPIException):
|
||||
# As this inherits from xmlrpclib.Fault it also has the
|
||||
# attributes faultCode and faultString.
|
||||
pass
|
||||
|
||||
class SipgateAPIProtocolError(ProtocolError, SipgateAPIException):
|
||||
# As this inherits from xmlrpclib.ProtocolError it also has the
|
||||
# attributes errcode and errmsg.
|
||||
pass
|
||||
|
||||
class SipgateAPISocketError(socket_error, SipgateAPIException):
|
||||
# As this inherits from socket.error it also has the
|
||||
# attributes .
|
||||
pass
|
||||
|
||||
### ------ This section contains message strings -------
|
||||
|
||||
UNKNOWN_METHOD_MESSAGE = "The method '%(method)s' for the API prefix '%(api_prefix)s' " + \
|
||||
"was called. This method, however, is currently not documented for the Sipgate API " + \
|
||||
"v%(api_version)s (%(api_date)s). Let's try but I've warned you.\n"
|
||||
DICT_AS_PARAM_MESSAGE = 'Please specify a dictionary as function call parameter for api.%s().'
|
||||
RESPONSE_NOT_A_DICTIONARY = 'The response "%s" does not seem to be a response from the ' + \
|
||||
'Sipgate XML-RPC API.'
|
||||
|
||||
### ------ This section contains constants of the Sipgate XML-RPC API -------
|
||||
|
||||
# This constant represents the version of the currently implemented Sipgate API
|
||||
# ans is taken from the API description PDF:
|
||||
SIPGATE_API_DOC_V = '1.06'
|
||||
SIPGATE_API_DOC_D = 'August 21, 2007'
|
||||
|
||||
# Sipgate basic and plus accounts must use this API URL:
|
||||
SIPGATE_API_URL = "https://%(username)s:%(password)s@samurai.sipgate.net/RPC2"
|
||||
# Sipgate one and team have a different URL: api.sipgate.net.
|
||||
# see <http://groups.google.com/group/sipgate-api/msg/51a3535b6d61241f>
|
||||
API_PREFIX = 'samurai.'
|
||||
|
||||
VALID_METHODS = [
|
||||
'AccountStatementGet',
|
||||
'BalanceGet',
|
||||
'ClientIdentify',
|
||||
'HistoryGetByDate',
|
||||
'ItemizedEntriesGet',
|
||||
'OwnUriListGet',
|
||||
'PhonebookEntryGet',
|
||||
'PhonebookListGet',
|
||||
'RecommendedIntervalGet',
|
||||
'ServerdataGet',
|
||||
'SessionClose',
|
||||
'SessionInitiate',
|
||||
'SessionInitiateMulti',
|
||||
'SessionStatusGet',
|
||||
'TosListGet',
|
||||
'TosListGet',
|
||||
'UmSummaryGet',
|
||||
'UserdataGreetingGet',
|
||||
'UserdataSipGet',
|
||||
]
|
||||
|
||||
SERVER_STATUS_CODES = {
|
||||
### From Table A.1 and A.2 of the API docu: general server status codes
|
||||
200: 'Method success',
|
||||
400: 'Method not supported',
|
||||
401: 'Request denied (no reason specified)',
|
||||
402: 'Internal error',
|
||||
403: 'Invalid arguments',
|
||||
404: 'Resources exceeded (this MUST not be used to indicate parameters in error)',
|
||||
405: 'Invalid parameter name',
|
||||
406: 'Invalid parameter type',
|
||||
407: 'Invalid parameter value',
|
||||
408: 'Attempt to set a non-writable parameter',
|
||||
409: 'Notification request rejected.',
|
||||
410: 'Parameter exceeds maximum size.',
|
||||
411: 'Missing parameter.',
|
||||
412: 'Too many requests.',
|
||||
500: 'Date out of range.',
|
||||
501: 'Uri does not belong to user.',
|
||||
502: 'Unknown type of service.',
|
||||
503: 'Selected payment method failed.',
|
||||
504: 'Selected currency not supported.',
|
||||
505: 'Amount exceeds limit.',
|
||||
506: 'Malformed SIP URI.',
|
||||
507: 'URI not in list.',
|
||||
508: 'Format is not valid E.164.',
|
||||
509: 'Unknown status.',
|
||||
510: 'Unknown ID.',
|
||||
511: 'Invalid timevalue.',
|
||||
512: 'Referenced session not found.',
|
||||
513: 'Only single default per TOS allowed.',
|
||||
514: 'Malformed VCARD format.',
|
||||
515: 'Malformed PID format.',
|
||||
516: 'Presence information not available.',
|
||||
517: 'Invalid label name.',
|
||||
518: 'Label not assigned.',
|
||||
519: 'Label doesn’t exist.',
|
||||
520: 'Parameter includes invalid characters.',
|
||||
521: 'Bad password. (Rejected due to security concerns.)',
|
||||
522: 'Malformed timezone format.',
|
||||
523: 'Delay exceeds limit.',
|
||||
524: 'Requested VPN type not available.',
|
||||
525: 'Requested TOS not available.',
|
||||
526: 'Unified messaging not available.',
|
||||
527: 'URI not available for registration.',
|
||||
}
|
||||
|
||||
TYPE_OF_SERVICE = {
|
||||
'fax': 'pages', # fax transmission
|
||||
'text': 'characters', # text message (e.g. "SMS")
|
||||
'video': 'seconds', # video communication
|
||||
'voice': 'seconds', # voice communication
|
||||
}
|
||||
|
||||
|
||||
class helpers (object):
|
||||
@staticmethod
|
||||
def FQTN(phone_number, default_country_code):
|
||||
"""
|
||||
Assures phone numbers are in the form of a E164 Fully Qualified Telephone Number
|
||||
without the leading + sign.
|
||||
The alternative would be the Python port of Google's libphonenumber:
|
||||
https://github.com/daviddrysdale/python-phonenumbers
|
||||
"""
|
||||
phone_number = phone_number.replace(' ','').replace('-','').replace('+','').replace('/','')
|
||||
|
||||
## number starting with 00 (so it's an international format)
|
||||
if re.compile("^00[1-9][0-9]*$").match(phone_number):
|
||||
return phone_number[2:]
|
||||
|
||||
## number starting with your country code (so it was already a FQTN):
|
||||
if re.compile("^"+default_country_code+"[1-9][0-9]*$").match(phone_number):
|
||||
return phone_number
|
||||
|
||||
if re.compile("^0[1-9]*$").match(phone_number):
|
||||
return default_country_code+phone_number[1:]
|
||||
|
||||
if re.compile("^[1-9]*$").match(phone_number):
|
||||
return phone_number
|
||||
|
||||
raise TypeError("Couldn't parse this phone number: "+phone_number)
|
99
cgi/sniff.py
99
cgi/sniff.py
|
@ -1,99 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import cgi
|
||||
import cgitb
|
||||
import time
|
||||
import pycurl
|
||||
import StringIO
|
||||
import json
|
||||
import sipgate
|
||||
|
||||
sys.path.insert(1, os.path.join(sys.path[0], '..'))
|
||||
|
||||
from constants import *
|
||||
|
||||
def send_sms(recipient, content):
|
||||
sg = sipgate.api(SIPGATE_USERNAME, SIPGATE_PASSWORD, 'transwhat')
|
||||
|
||||
default_uri = 'sip:NULL@sipgate.net'
|
||||
for own_uri in sg.OwnUriListGet()['OwnUriList']:
|
||||
if own_uri['DefaultUri']:
|
||||
default_uri = own_uri['SipUri']
|
||||
|
||||
# SessionInitiate may return the following server status codes in case of errors: 501, 502, 506, 520, 525
|
||||
return sg.SessionInitiate({'LocalUri': default_uri, 'RemoteUri': 'sip:%s@sipgate.de' % recipient, 'TOS': 'text', 'Content': content })
|
||||
|
||||
def main():
|
||||
url = os.environ['SCRIPT_URI'] + '?' + os.environ['QUERY_STRING']
|
||||
|
||||
writer = StringIO.StringIO()
|
||||
ch = pycurl.Curl()
|
||||
|
||||
ch.setopt(pycurl.URL, url)
|
||||
ch.setopt(pycurl.USERAGENT, os.environ['HTTP_USER_AGENT'])
|
||||
|
||||
ch.setopt(pycurl.WRITEFUNCTION, writer.write)
|
||||
ch.setopt(pycurl.SSL_VERIFYPEER, False)
|
||||
ch.setopt(pycurl.HEADER, True)
|
||||
|
||||
ch.perform()
|
||||
|
||||
response = writer.getvalue()
|
||||
headers, body = response.split("\r\n\r\n", 1)
|
||||
headers = headers.split("\n")
|
||||
preamble = headers.pop(0)
|
||||
|
||||
code = preamble.split(" ", 2)[1]
|
||||
status = preamble.split(" ", 2)[2]
|
||||
|
||||
print "Status: %s %s" % (code, status)
|
||||
for header in headers:
|
||||
print header
|
||||
|
||||
print
|
||||
print body
|
||||
|
||||
file = open(REQUESTS_FILE, "a")
|
||||
file.write("\n--- Time: %s\n>>> Request: %s\n<<< Reponse Headers:\n%s\nResponse Body:\n%s\n" % (time.strftime("%a, %d %b %Y %H:%M:%S"), url, "\n".join(headers), body))
|
||||
file.close()
|
||||
|
||||
# send password via sms to requester
|
||||
if code == "200":
|
||||
parsed = json.loads(body)
|
||||
form = cgi.FieldStorage()
|
||||
cc = form.getfirst("cc")
|
||||
number = form.getfirst("in")
|
||||
|
||||
if parsed.has_key('pw') and parsed.has_key('login'):
|
||||
send_sms(parsed['login'], parsed['pw'])
|
||||
|
||||
ch.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,63 +0,0 @@
|
|||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@0l.de
|
||||
ServerName whatsapp.0l.de
|
||||
|
||||
DocumentRoot /home/stv0g/files/whatsapp/transwhat/cgi
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
</Directory>
|
||||
<Directory /home/stv0g/files/whatsapp/transwhat/cgi/>
|
||||
Options Indexes FollowSymLinks MultiViews +ExecCGI
|
||||
AllowOverride All
|
||||
Order allow,deny
|
||||
allow from all
|
||||
AddHandler cgi-script .py
|
||||
</Directory>
|
||||
|
||||
ErrorLog /home/stv0g/files/whatsapp/htdocs/error.log
|
||||
CustomLog /home/stv0g/files/whatsapp/htdocs/access.log combined
|
||||
</VirtualHost>
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin webmaster@0l.de
|
||||
ServerName whatsapp.0l.de
|
||||
ServerAlias v.whatsapp.net
|
||||
|
||||
DocumentRoot /home/stv0g/files/whatsapp/transwhat/cgi
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
</Directory>
|
||||
<Directory /home/stv0g/files/whatsapp/transwhat/cgi/>
|
||||
Options Indexes FollowSymLinks MultiViews +ExecCGI
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
AddHandler cgi-script .py
|
||||
</Directory>
|
||||
|
||||
ErrorLog /home/stv0g/files/whatsapp/htdocs/error.log
|
||||
CustomLog /home/stv0g/files/whatsapp/htdocs/access.log combined
|
||||
|
||||
LogLevel info
|
||||
# debug, info, notice, warn, error, crit, alert, emerg.
|
||||
|
||||
# Rewrite
|
||||
RewriteEngine on
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule (.*) /sniff.py/$1
|
||||
|
||||
# SSL
|
||||
SSLEngine on
|
||||
SSLCertificateFile /home/stv0g/files/whatsapp/htdocs/whatsapp.crt
|
||||
SSLCertificateKeyFile /home/stv0g/files/whatsapp/htdocs/whatsapp.key
|
||||
|
||||
<FilesMatch "\.(cgi|shtml|phtml|php)$">
|
||||
SSLOptions +StdEnvVars
|
||||
</FilesMatch>
|
||||
</VirtualHost>
|
||||
</IfModule>
|
|
@ -1,9 +0,0 @@
|
|||
Welcome to transWhat!
|
||||
===== NEWS ====
|
||||
- 03.06.13 transWhat service is born
|
||||
- 10.06.13 added bot user to import contacts and adjust settings, see http://2p.0l.de
|
||||
- 14.06.13 finally enable password sniffing, see http://2o.0l.de
|
||||
- 18.06.13 major deployment of development version
|
||||
|
||||
Type "\help" for a list of available commands.
|
||||
Visit http://whatsapp.0l.de for the full documentation.
|
|
@ -1,30 +0,0 @@
|
|||
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8 */;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `buddies` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`owner_id` int(11) NOT NULL,
|
||||
`buddy_id` int(11) NOT NULL,
|
||||
`nick` varchar(255) NOT NULL,
|
||||
`groups` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `numbers` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`number` varchar(32) NOT NULL,
|
||||
`picture` blob NOT NULL,
|
||||
`state` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `number` (`number`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
|
@ -1,29 +0,0 @@
|
|||
[service]
|
||||
server_mode = 1
|
||||
|
||||
user = spectrum
|
||||
group = spectrum
|
||||
|
||||
jid = whatsapp-dev.0l.de
|
||||
|
||||
server = 0.0.0.0
|
||||
port = 5221
|
||||
|
||||
cert = /home/stv0g/files/whatsapp/transwhat/conf/spectrum_cert.p12
|
||||
|
||||
backend_host = localhost
|
||||
backend = /home/stv0g/files/whatsapp/transwhat/transwhat.py
|
||||
users_per_backend = 10
|
||||
more_resources = 1
|
||||
|
||||
admin_jid = 4917696978528@whatsapp.0l.de
|
||||
|
||||
[identity]
|
||||
name = transWhat
|
||||
|
||||
type = xmpp
|
||||
category = gateway
|
||||
|
||||
[logging]
|
||||
config = /etc/spectrum2/logging.cfg
|
||||
backend_config = /etc/spectrum2/backend-logging.cfg
|
|
@ -1,40 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
DB_HOST = "localhost"
|
||||
DB_USER = ""
|
||||
DB_PASS = ""
|
||||
DB_TABLE = "transwhat"
|
||||
|
||||
BASE_PATH = "/opt/transwhat"
|
||||
|
||||
TOKEN_FILE = BASE_PATH + "/logs/tokens"
|
||||
MOTD_FILE = BASE_PATH + "/conf/motd"
|
||||
REQUESTS_FILE = BASE_PATH + "/logs/requests"
|
||||
|
||||
GOOGLE_CLIENT_ID = ""
|
||||
GOOGLE_CLIENT_SECRET = ""
|
||||
|
||||
SIPGATE_USERNAME=""
|
||||
SIPGATE_PASSWORD=""
|
|
@ -0,0 +1,11 @@
|
|||
[Unit]
|
||||
description=spectrum2 whatsapp
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/spectrum2 --no-daemonize -j whatsapp.example.com --config /etc/spectrum2/transports/whatsapp.cfg
|
||||
Restart=always
|
||||
User=spectrum2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,4 @@
|
|||
Component "whatsapp.0l.de"
|
||||
component_secret = "whatsappsucks"
|
||||
component_ports = { 5221 }
|
||||
component_interface = "127.0.0.1"
|
|
@ -1,28 +1,30 @@
|
|||
[service]
|
||||
server_mode = 1
|
||||
|
||||
user = spectrum
|
||||
group = spectrum
|
||||
|
||||
jid = whatsapp.0l.de
|
||||
|
||||
server = 0.0.0.0
|
||||
port = 5222
|
||||
|
||||
cert = /opt/transwhat/conf/spectrum_cert.p12
|
||||
server = localhost
|
||||
password = whatsappsucks
|
||||
port = 5221
|
||||
|
||||
backend_host = localhost
|
||||
backend = /opt/transwhat/transwhat.py
|
||||
users_per_backend = 10
|
||||
backend = /usr/bin/transwhat
|
||||
|
||||
admin_jid = 4917696978528@whatsapp.0l.de
|
||||
users_per_backend = 10
|
||||
more_resources = 1
|
||||
|
||||
admin_jid = your@jid.example
|
||||
|
||||
[identity]
|
||||
name = transWhat
|
||||
|
||||
type = xmpp
|
||||
category = gateway
|
||||
|
||||
[logging]
|
||||
config = /etc/spectrum2/logging.cfg
|
||||
backend_config = /etc/spectrum2/backend-logging.cfg
|
||||
|
||||
[database]
|
||||
type = sqlite3
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import gdata.gauth
|
||||
import gdata.contacts.client
|
||||
import gdata.contacts.data
|
||||
import atom.data
|
||||
|
||||
from constants import *
|
||||
|
||||
gdata.contacts.REL_MOBILE='http://schemas.google.com/g/2005#mobile'
|
||||
|
||||
class GoogleClient():
|
||||
|
||||
def __init__(self):
|
||||
self.client = gdata.contacts.client.ContactsClient()
|
||||
self.token = gdata.gauth.OAuth2Token(
|
||||
client_id = GOOGLE_CLIENT_ID,
|
||||
client_secret = GOOGLE_CLIENT_SECRET,
|
||||
scope = 'https://www.google.com/m8/feeds/contacts',
|
||||
user_agent = 'whatTrans'
|
||||
)
|
||||
|
||||
def getTokenUrl(self, uri = 'urn:ietf:wg:oauth:2.0:oob'):
|
||||
return self.token.generate_authorize_url(redirect_uri=uri)
|
||||
|
||||
def getContacts(self, request_token):
|
||||
access_token = self.token.get_access_token(request_token)
|
||||
|
||||
self.token.authorize(self.client)
|
||||
|
||||
numbers = { }
|
||||
|
||||
feed = self.client.GetContacts()
|
||||
while feed:
|
||||
for i, entry in enumerate(feed.entry):
|
||||
for number in entry.phone_number:
|
||||
numbers[number.text] = entry.title.text
|
||||
|
||||
next = feed.GetNextLink()
|
||||
if next:
|
||||
feed = self.client.GetContacts(next.href)
|
||||
else:
|
||||
break
|
||||
|
||||
return numbers
|
34
group.py
34
group.py
|
@ -1,34 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
class Group():
|
||||
|
||||
def __init__(self, id, owner, subject, subjectOwner):
|
||||
self.id = id
|
||||
self.subject = subject
|
||||
self.subjectOwner = subjectOwner
|
||||
self.owner = owner
|
||||
|
||||
self.nick = "me"
|
||||
self.participants = { }
|
49
reader.py
49
reader.py
|
@ -1,49 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
def get_token(number, timeout = 30):
|
||||
file = open('tokens')
|
||||
file.seek(-1, 2)
|
||||
|
||||
count = 0
|
||||
while count < timeout:
|
||||
line = file.readline()
|
||||
|
||||
if line in ["", "\n"]:
|
||||
time.sleep(1)
|
||||
count += 1
|
||||
continue
|
||||
else:
|
||||
t, n, tk = line[:-1].split("\t")
|
||||
|
||||
if (n == number):
|
||||
file.close()
|
||||
return tk
|
||||
|
||||
file.close()
|
||||
|
||||
|
||||
print get_token("4917696978528")
|
439
session.py
439
session.py
|
@ -1,439 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import utils
|
||||
import logging
|
||||
import urllib
|
||||
import time
|
||||
|
||||
from Yowsup.connectionmanager import YowsupConnectionManager
|
||||
from Spectrum2 import protocol_pb2
|
||||
|
||||
from buddy import BuddyList
|
||||
from threading import Timer
|
||||
from group import Group
|
||||
from bot import Bot
|
||||
from constants import *
|
||||
|
||||
class Session:
|
||||
|
||||
def __init__(self, backend, user, legacyName, extra, db):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.logger.info("Created: %s", legacyName)
|
||||
|
||||
self.db = db
|
||||
self.backend = backend
|
||||
self.user = user
|
||||
self.legacyName = legacyName
|
||||
|
||||
self.status = protocol_pb2.STATUS_NONE
|
||||
self.statusMessage = ''
|
||||
|
||||
self.groups = {}
|
||||
self.presenceRequested = []
|
||||
self.offlineQueue = []
|
||||
self.groupOfflineQueue = { }
|
||||
|
||||
self.timer = None
|
||||
self.password = None
|
||||
self.initialized = False
|
||||
|
||||
self.buddies = BuddyList(legacyName, db)
|
||||
self.frontend = YowsupConnectionManager()
|
||||
|
||||
self.bot = Bot(self)
|
||||
|
||||
# Events
|
||||
self.listen("auth_success", self.onAuthSuccess)
|
||||
self.listen("auth_fail", self.onAuthFailed)
|
||||
self.listen("disconnected", self.onDisconnected)
|
||||
|
||||
self.listen("contact_typing", self.onContactTyping)
|
||||
self.listen("contact_paused", self.onContactPaused)
|
||||
|
||||
self.listen("presence_updated", self.onPrecenceUpdated)
|
||||
self.listen("presence_available", self.onPrecenceAvailable)
|
||||
self.listen("presence_unavailable", self.onPrecenceUnavailable)
|
||||
|
||||
self.listen("message_received", self.onMessageReceived)
|
||||
self.listen("image_received", self.onMediaReceived)
|
||||
self.listen("video_received", self.onMediaReceived)
|
||||
self.listen("audio_received", self.onMediaReceived)
|
||||
self.listen("location_received", self.onLocationReceived)
|
||||
self.listen("vcard_received", self.onVcardReceived)
|
||||
|
||||
self.listen("group_messageReceived", self.onGroupMessageReceived)
|
||||
self.listen("group_gotInfo", self.onGroupGotInfo)
|
||||
self.listen("group_gotParticipants", self.onGroupGotParticipants)
|
||||
self.listen("group_subjectReceived", self.onGroupSubjectReceived)
|
||||
|
||||
self.listen("notification_groupParticipantAdded", self.onGroupParticipantAdded)
|
||||
self.listen("notification_groupParticipantRemoved", self.onGroupParticipantRemoved)
|
||||
self.listen("notification_contactProfilePictureUpdated", self.onContactProfilePictureUpdated)
|
||||
self.listen("notification_groupPictureUpdated", self.onGroupPictureUpdated)
|
||||
|
||||
def __del__(self): # handleLogoutRequest
|
||||
self.logout()
|
||||
|
||||
def call(self, method, args = ()):
|
||||
args = [str(s) for s in args]
|
||||
self.logger.debug("%s(%s)", method, ", ".join(args))
|
||||
self.frontend.methodInterface.call(method, args)
|
||||
|
||||
def listen(self, event, callback):
|
||||
self.frontend.signalInterface.registerListener(event, callback)
|
||||
|
||||
def logout(self):
|
||||
self.call("disconnect", ("logout",))
|
||||
|
||||
def login(self, password):
|
||||
self.password = utils.decodePassword(password)
|
||||
self.call("auth_login", (self.legacyName, self.password))
|
||||
|
||||
def updateRoomList(self):
|
||||
rooms = []
|
||||
for room, group in self.groups.iteritems():
|
||||
rooms.append([room, group.subject])
|
||||
|
||||
self.backend.handleRoomList(rooms)
|
||||
|
||||
# spectrum RequestMethods
|
||||
def sendTypingStarted(self, buddy):
|
||||
if buddy != "bot":
|
||||
self.logger.info("Started typing: %s to %s", self.legacyName, buddy)
|
||||
self.call("typing_send", (buddy + "@s.whatsapp.net",))
|
||||
|
||||
def sendTypingStopped(self, buddy):
|
||||
if buddy != "bot":
|
||||
self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy)
|
||||
self.call("typing_paused", (buddy + "@s.whatsapp.net",))
|
||||
|
||||
def sendMessageToWA(self, sender, message):
|
||||
self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message)
|
||||
message = message.encode("utf-8")
|
||||
|
||||
if sender == "bot":
|
||||
self.bot.parse(message)
|
||||
elif "-" in sender: # group msg
|
||||
if "/" in sender:
|
||||
room, buddy = sender.split("/")
|
||||
self.call("message_send", (buddy + "@s.whatsapp.net", message))
|
||||
else:
|
||||
room = sender
|
||||
group = self.groups[room]
|
||||
|
||||
self.backend.handleMessage(self.user, room, message, group.nick)
|
||||
self.call("message_send", (room + "@g.us", message))
|
||||
else: # private msg
|
||||
buddy = sender
|
||||
if message == "\\lastseen":
|
||||
self.presenceRequested.append(buddy)
|
||||
self.call("presence_request", (buddy + "@s.whatsapp.net",))
|
||||
else:
|
||||
self.call("message_send", (buddy + "@s.whatsapp.net", message))
|
||||
|
||||
def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""):
|
||||
if timestamp:
|
||||
timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp))
|
||||
|
||||
if self.initialized == False:
|
||||
self.logger.debug("Message queued from %s to %s: %s", buddy, self.legacyName, messageContent)
|
||||
self.offlineQueue.append((buddy, messageContent, timestamp))
|
||||
else:
|
||||
self.logger.debug("Message sent from %s to %s: %s", buddy, self.legacyName, messageContent)
|
||||
self.backend.handleMessage(self.user, buddy, messageContent, "", "", timestamp)
|
||||
|
||||
def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""):
|
||||
if timestamp:
|
||||
timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp))
|
||||
|
||||
if self.initialized == False:
|
||||
self.logger.debug("Group message queued from %s to %s: %s", buddy, room, messageContent)
|
||||
|
||||
if room not in self.groupOfflineQueue:
|
||||
self.groupOfflineQueue[room] = [ ]
|
||||
|
||||
self.groupOfflineQueue[room].append((buddy, messageContent, timestamp))
|
||||
else:
|
||||
self.logger.debug("Group message sent from %s to %s: %s", buddy, room, messageContent)
|
||||
self.backend.handleMessage(self.user, room, messageContent, buddy, "", timestamp)
|
||||
|
||||
def changeStatus(self, status):
|
||||
if status != self.status:
|
||||
self.logger.info("Status changed: %s", status)
|
||||
self.status = status
|
||||
|
||||
if status == protocol_pb2.STATUS_ONLINE or status == protocol_pb2.STATUS_FFC:
|
||||
self.call("presence_sendAvailable")
|
||||
else:
|
||||
self.call("presence_sendUnavailable")
|
||||
|
||||
def changeStatusMessage(self, statusMessage):
|
||||
if (statusMessage != self.statusMessage) or (self.initialized == False):
|
||||
self.statusMessage = statusMessage
|
||||
self.call("profile_setStatus", (statusMessage.encode("utf-8"),))
|
||||
self.logger.info("Status message changed: %s", statusMessage)
|
||||
|
||||
if self.initialized == False:
|
||||
self.sendOfflineMessages()
|
||||
self.bot.call("welcome")
|
||||
self.initialized = True
|
||||
|
||||
def sendOfflineMessages(self):
|
||||
# Flush Queues
|
||||
while self.offlineQueue:
|
||||
msg = self.offlineQueue.pop(0)
|
||||
self.backend.handleMessage(self.user, msg[0], msg[1], "", "", msg[2])
|
||||
|
||||
# also for adding a new buddy
|
||||
def updateBuddy(self, buddy, nick, groups):
|
||||
if buddy != "bot":
|
||||
self.buddies.update(buddy, nick, groups)
|
||||
self.updateRoster()
|
||||
|
||||
def removeBuddy(self, buddy):
|
||||
if buddy != "bot":
|
||||
self.logger.info("Buddy removed: %s", buddy)
|
||||
self.buddies.remove(buddy)
|
||||
self.updateRoster()
|
||||
|
||||
def joinRoom(self, room, nick):
|
||||
if room in self.groups:
|
||||
group = self.groups[room]
|
||||
|
||||
self.logger.info("Joining room: %s room=%s, nick=%s", self.legacyName, room, nick)
|
||||
|
||||
group.nick = nick
|
||||
|
||||
self.call("group_getParticipants", (room + "@g.us",))
|
||||
self.backend.handleSubject(self.user, room, group.subject, group.subjectOwner)
|
||||
else:
|
||||
self.logger.warn("Room doesn't exist: %s", room)
|
||||
|
||||
def updateRoster(self):
|
||||
self.logger.debug("Update roster")
|
||||
|
||||
old = self.buddies.keys()
|
||||
self.buddies.load()
|
||||
new = self.buddies.keys()
|
||||
|
||||
add = set(new) - set(old)
|
||||
remove = set(old) - set(new)
|
||||
|
||||
self.logger.debug("Roster remove: %s", str(list(remove)))
|
||||
self.logger.debug("Roster add: %s", str(list(add)))
|
||||
|
||||
for number in remove:
|
||||
self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE)
|
||||
self.backend.handleBuddyRemoved(self.user, number)
|
||||
self.call("presence_unsubscribe", (number + "@s.whatsapp.net",))
|
||||
|
||||
for number in add:
|
||||
buddy = self.buddies[number]
|
||||
self.backend.handleBuddyChanged(self.user, number, buddy.nick, buddy.groups, protocol_pb2.STATUS_NONE)
|
||||
self.call("presence_request", (number + "@s.whatsapp.net",)) # includes presence_subscribe
|
||||
|
||||
|
||||
# yowsup Signals
|
||||
def onAuthSuccess(self, user):
|
||||
self.logger.info("Auth success: %s", user)
|
||||
|
||||
self.backend.handleConnected(self.user)
|
||||
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE)
|
||||
|
||||
self.updateRoster()
|
||||
|
||||
self.call("ready")
|
||||
self.call("group_getGroups", ("participating",))
|
||||
|
||||
def onAuthFailed(self, user, reason):
|
||||
self.logger.info("Auth failed: %s (%s)", user, reason)
|
||||
self.backend.handleDisconnected(self.user, 0, reason)
|
||||
self.password = None
|
||||
|
||||
def onDisconnected(self, reason):
|
||||
self.logger.info("Disconnected from whatsapp: %s (%s)", self.legacyName, reason)
|
||||
self.backend.handleDisconnected(self.user, 0, reason)
|
||||
|
||||
def onMessageReceived(self, messageId, jid, messageContent, timestamp, receiptRequested, pushName, isBroadCast):
|
||||
buddy = jid.split("@")[0]
|
||||
messageContent = utils.softToUni(messageContent)
|
||||
|
||||
if isBroadCast:
|
||||
self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp)
|
||||
messageContent = "[Broadcast] " + messageContent
|
||||
else:
|
||||
self.logger.info("Message received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp)
|
||||
|
||||
self.sendMessageToXMPP(buddy, messageContent, timestamp)
|
||||
if receiptRequested: self.call("message_ack", (jid, messageId))
|
||||
|
||||
def onMediaReceived(self, messageId, jid, preview, url, size, receiptRequested, isBroadcast):
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
self.logger.info("Media received from %s: %s", buddy, url)
|
||||
self.sendMessageToXMPP(buddy, utils.shorten(url))
|
||||
if receiptRequested: self.call("message_ack", (jid, messageId))
|
||||
|
||||
def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast):
|
||||
buddy = jid.split("@")[0]
|
||||
self.logger.info("Location received from %s: %s, %s", buddy, latitude, longitude)
|
||||
|
||||
url = "http://maps.google.de?%s" % urllib.urlencode({ "q": "%s %s" % (latitude, longitude) })
|
||||
self.sendMessageToXMPP(buddy, utils.shorten(url))
|
||||
if receiptRequested: self.call("message_ack", (jid, messageId))
|
||||
|
||||
def onVcardReceived(self, messageId, jid, name, data, receiptRequested, isBroadcast): # TODO
|
||||
buddy = jid.split("@")[0]
|
||||
self.logger.info("VCard received from %s", buddy)
|
||||
self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)")
|
||||
if receiptRequested: self.call("message_ack", (jid, messageId))
|
||||
|
||||
def onContactTyping(self, jid):
|
||||
buddy = jid.split("@")[0]
|
||||
self.logger.info("Started typing: %s", buddy)
|
||||
self.backend.handleBuddyTyping(self.user, buddy)
|
||||
|
||||
if self.timer != None:
|
||||
self.timer.cancel()
|
||||
|
||||
def onContactPaused(self, jid):
|
||||
buddy = jid.split("@")[0]
|
||||
self.logger.info("Paused typing: %s", buddy)
|
||||
self.backend.handleBuddyTyped(self.user, jid.split("@")[0])
|
||||
self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start()
|
||||
|
||||
def onPrecenceUpdated(self, jid, lastseen):
|
||||
buddy = jid.split("@")[0]
|
||||
self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen))
|
||||
|
||||
if buddy in self.presenceRequested:
|
||||
timestamp = time.localtime(time.time() - lastseen)
|
||||
timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp)
|
||||
self.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen)))
|
||||
self.presenceRequested.remove(buddy)
|
||||
|
||||
if lastseen < 60:
|
||||
self.onPrecenceAvailable(jid)
|
||||
else:
|
||||
self.onPrecenceUnavailable(jid)
|
||||
|
||||
def onPrecenceAvailable(self, jid):
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
try:
|
||||
buddy = self.buddies[buddy]
|
||||
self.logger.info("Is available: %s", buddy)
|
||||
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
|
||||
except KeyError:
|
||||
self.logger.error("Buddy not found: %s", buddy)
|
||||
|
||||
def onPrecenceUnavailable(self, jid):
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
try:
|
||||
buddy = self.buddies[buddy]
|
||||
self.logger.info("Is unavailable: %s", buddy)
|
||||
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA)
|
||||
except KeyError:
|
||||
self.logger.error("Buddy not found: %s", buddy)
|
||||
|
||||
def onGroupGotInfo(self, gjid, owner, subject, subjectOwner, subjectTimestamp, creationTimestamp):
|
||||
room = gjid.split("@")[0]
|
||||
owner = owner.split("@")[0]
|
||||
subjectOwner = subjectOwner.split("@")[0]
|
||||
|
||||
if room in self.groups:
|
||||
room = self.groups[room]
|
||||
room.owner = owner
|
||||
room.subjectOwner = subjectOwner
|
||||
room.subject = subject
|
||||
else:
|
||||
self.groups[room] = Group(room, owner, subject, subjectOwner)
|
||||
|
||||
self.updateRoomList()
|
||||
|
||||
def onGroupGotParticipants(self, gjid, jids):
|
||||
room = gjid.split("@")[0]
|
||||
group = self.groups[room]
|
||||
|
||||
for jid in jids:
|
||||
buddy = jid.split("@")[0]
|
||||
self.logger.info("Added %s to room %s", buddy, room)
|
||||
|
||||
if buddy == group.owner:
|
||||
flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
|
||||
else:
|
||||
flags = protocol_pb2.PARTICIPANT_FLAG_NONE
|
||||
|
||||
self.backend.handleParticipantChanged(self.user, buddy, room, flags, protocol_pb2.STATUS_ONLINE) # TODO check status
|
||||
|
||||
if room in self.groupOfflineQueue:
|
||||
while self.groupOfflineQueue[room]:
|
||||
msg = self.groupOfflineQueue[room].pop(0)
|
||||
self.backend.handleMessage(self.user, room, msg[1], msg[0], "", msg[2])
|
||||
self.logger.debug("Send queued group message to: %s %s %s", msg[0],msg[1], msg[2])
|
||||
|
||||
def onGroupMessageReceived(self, messageId, gjid, jid, messageContent, timestamp, receiptRequested, pushName):
|
||||
buddy = jid.split("@")[0]
|
||||
room = gjid.split("@")[0]
|
||||
|
||||
self.logger.info("Group message received in %s from %s: %s", room, buddy, messageContent)
|
||||
|
||||
self.sendGroupMessageToXMPP(room, buddy, utils.softToUni(messageContent), timestamp)
|
||||
if receiptRequested: self.call("message_ack", (gjid, messageId))
|
||||
|
||||
def onGroupSubjectReceived(self, messageId, gjid, jid, subject, timestamp, receiptRequested):
|
||||
room = gjid.split("@")[0]
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
self.backend.handleSubject(self.user, room, subject, buddy)
|
||||
if receiptRequested: self.call("subject_ack", (gjid, messageId))
|
||||
|
||||
# Yowsup Notifications
|
||||
def onGroupParticipantAdded(self, gjid, jid, author, timestamp, messageId, receiptRequested):
|
||||
room = gjid.split("@")[0]
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
loggin.info("Added % to room %s", buddy, room)
|
||||
|
||||
self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE)
|
||||
if receiptRequested: self.call("notification_ack", (gjid, messageId))
|
||||
|
||||
def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested):
|
||||
room = gjid.split("@")[0]
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
self.logger.info("Removed %s from room %s", buddy, room)
|
||||
|
||||
self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_NONE) # TODO
|
||||
if receiptRequested: self.call("notification_ack", (gjid, messageId))
|
||||
|
||||
def onContactProfilePictureUpdated(self, jid, timestamp, messageId, pictureId, receiptRequested):
|
||||
# TODO
|
||||
if receiptRequested: self.call("notification_ack", (jid, messageId))
|
||||
|
||||
def onGroupPictureUpdated(self, jid, author, timestamp, messageId, pictureId, receiptRequested):
|
||||
# TODO
|
||||
if receiptRequested: self.call("notification_ack", (jid, messageId))
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import codecs
|
||||
from setuptools import setup
|
||||
|
||||
def read_file(filename, encoding='utf8'):
|
||||
"""Read unicode from given file."""
|
||||
with codecs.open(filename, encoding=encoding) as fd:
|
||||
return fd.read()
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
readme = read_file(os.path.join(here, 'README.rst'))
|
||||
|
||||
setup(name='transwhat',
|
||||
version='0.2.2',
|
||||
description='A gateway between the XMPP and the WhatsApp IM networks',
|
||||
long_description=readme,
|
||||
keywords='whatsapp xmpp im gateway transport yowsup',
|
||||
url='https://github.com/stv0g/transwhat',
|
||||
author='Steffen Vogel',
|
||||
author_email='stv0g@0l.de',
|
||||
python_requires='>=3.5',
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Plugins',
|
||||
'Operating System :: POSIX',
|
||||
'Topic :: Communications :: Chat'
|
||||
],
|
||||
license='GPL-3+',
|
||||
packages=[
|
||||
'transWhat'
|
||||
],
|
||||
scripts=[
|
||||
'transWhat/transwhat.py'
|
||||
],
|
||||
install_requires=[
|
||||
'protobuf',
|
||||
'yowsup',
|
||||
'pyspectrum2',
|
||||
'python-dateutil',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['transwhat=transWhat.transwhat:main'],
|
||||
},
|
||||
zip_safe=False,
|
||||
include_package_data=True
|
||||
)
|
|
@ -0,0 +1,73 @@
|
|||
import threading
|
||||
import inspect
|
||||
import re
|
||||
import urllib
|
||||
import time
|
||||
import os
|
||||
|
||||
class Bot():
|
||||
def __init__(self, session, name = "Bot"):
|
||||
self.session = session
|
||||
self.name = name
|
||||
|
||||
self.commands = {
|
||||
"help": self._help,
|
||||
"groups": self._groups,
|
||||
"getgroups": self._getgroups
|
||||
}
|
||||
|
||||
def parse(self, message):
|
||||
args = message.strip().split(" ")
|
||||
cmd = args.pop(0)
|
||||
|
||||
if len(cmd) > 0 and cmd[0] == '\\':
|
||||
try:
|
||||
self.call(cmd[1:], args)
|
||||
except KeyError:
|
||||
self.send("invalid command")
|
||||
except TypeError:
|
||||
self.send("invalid syntax")
|
||||
else:
|
||||
self.send("a valid command starts with a backslash")
|
||||
|
||||
def call(self, cmd, args = []):
|
||||
func = self.commands[cmd.lower()]
|
||||
spec = inspect.getargspec(func)
|
||||
maxs = len(spec.args) - 1
|
||||
reqs = maxs - len(spec.defaults or [])
|
||||
if (reqs > len(args)) or (len(args) > maxs):
|
||||
raise TypeError()
|
||||
|
||||
thread = threading.Thread(target=func, args=tuple(args))
|
||||
thread.start()
|
||||
|
||||
def send(self, message):
|
||||
self.session.backend.handleMessage(self.session.user, self.name, message)
|
||||
|
||||
# commands
|
||||
def _help(self):
|
||||
self.send("""following bot commands are available:
|
||||
\\help show thi
|
||||
|
||||
following user commands are available:
|
||||
\\lastseen
|
||||
|
||||
following group commands are available
|
||||
\\leave permanentlhat
|
||||
\\groups print all attended groups
|
||||
\\getgroups get current groups from WA""")
|
||||
|
||||
def _groups(self):
|
||||
for group in self.session.groups:
|
||||
buddy = self.session.groups[group].owner
|
||||
try:
|
||||
nick = self.session.buddies[buddy].nick
|
||||
except KeyError:
|
||||
nick = buddy
|
||||
|
||||
self.send(self.session.groups[group].id + "@" + self.session.backend.spectrum_jid + " " + self.session.groups[group].subject + " Owner: " + nick )
|
||||
|
||||
def _getgroups(self):
|
||||
#self.session.call("group_getGroups", ("participating",))
|
||||
self.session.requestGroupsList(self.session._updateGroups)
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
import logging
|
||||
import time
|
||||
import base64
|
||||
import hashlib
|
||||
import Spectrum2
|
||||
|
||||
from . import deferred
|
||||
|
||||
class Buddy():
|
||||
def __init__(self, owner, number, nick, statusMsg, groups, image_hash):
|
||||
self.nick = nick
|
||||
self.owner = owner
|
||||
self.number = "%s" % number
|
||||
self.groups = groups
|
||||
self.image_hash = image_hash if image_hash is not None else ""
|
||||
self.statusMsg = u""
|
||||
self.lastseen = 0
|
||||
self.presence = 0
|
||||
|
||||
def update(self, nick, groups, image_hash):
|
||||
self.nick = nick
|
||||
self.groups = groups
|
||||
if image_hash is not None:
|
||||
self.image_hash = image_hash
|
||||
|
||||
def __str__(self):
|
||||
# we must return str here
|
||||
return str("%s (nick=%s)") % (self.number, self.nick)
|
||||
|
||||
class BuddyList(dict):
|
||||
|
||||
def __init__(self, owner, backend, user, session):
|
||||
self.owner = owner
|
||||
self.backend = backend
|
||||
self.session = session
|
||||
self.user = user
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def _load(self, buddies):
|
||||
for buddy in buddies:
|
||||
number = buddy.buddyName
|
||||
nick = buddy.alias
|
||||
statusMsg = buddy.statusMessage
|
||||
groups = [g for g in buddy.group]
|
||||
image_hash = buddy.iconHash
|
||||
self[number] = Buddy(self.owner, number, nick, statusMsg,
|
||||
groups, image_hash)
|
||||
|
||||
self.logger.debug("Update roster")
|
||||
|
||||
contacts = self.keys()
|
||||
contacts.remove('bot')
|
||||
|
||||
self.session.sendSync(contacts, delta=False, interactive=True,
|
||||
success=self.onSync)
|
||||
|
||||
self.logger.debug("Roster add: %s" % list(contacts))
|
||||
|
||||
for number in contacts:
|
||||
buddy = self[number]
|
||||
self.updateSpectrum(buddy)
|
||||
|
||||
def onSync(self, existing, nonexisting, invalid):
|
||||
"""We should only presence subscribe to existing numbers"""
|
||||
|
||||
for number in existing:
|
||||
self.session.subscribePresence(number)
|
||||
self.logger.debug("%s is requesting statuses of: %s" % (self.user, existing))
|
||||
self.session.requestStatuses(existing, success = self.onStatus)
|
||||
|
||||
self.logger.debug("Removing nonexisting buddies %s" % nonexisting)
|
||||
for number in nonexisting:
|
||||
self.remove(number)
|
||||
try: del self[number]
|
||||
except KeyError: self.logger.warn("non-existing buddy really didn't exist: %s" % number)
|
||||
|
||||
self.logger.debug("Removing invalid buddies %s" % invalid)
|
||||
for number in invalid:
|
||||
self.remove(number)
|
||||
try: del self[number]
|
||||
except KeyError: self.logger.warn("non-existing buddy really didn't exist: %s" % number)
|
||||
|
||||
def onStatus(self, contacts):
|
||||
self.logger.debug("%s received statuses of: %s" % (self.user, contacts))
|
||||
for number, (status, time) in contacts.iteritems():
|
||||
try: buddy = self[number]
|
||||
except KeyError: self.logger.warn("received status of buddy not in list: %s" % number)
|
||||
if status is None:
|
||||
buddy.statusMsg = ""
|
||||
else:
|
||||
buddy.statusMsg = status
|
||||
self.updateSpectrum(buddy)
|
||||
|
||||
def load(self, buddies):
|
||||
if self.session.loggedIn:
|
||||
self._load(buddies)
|
||||
else:
|
||||
self.session.loginQueue.append(lambda: self._load(buddies))
|
||||
|
||||
def update(self, number, nick, groups, image_hash):
|
||||
if number in self:
|
||||
buddy = self[number]
|
||||
buddy.update(nick, groups, image_hash)
|
||||
else:
|
||||
buddy = Buddy(self.owner, number, nick, "", groups, image_hash)
|
||||
self[number] = buddy
|
||||
self.logger.debug("Roster add: %s" % buddy)
|
||||
self.session.sendSync([number], delta = True, interactive = True)
|
||||
self.session.subscribePresence(number)
|
||||
self.session.requestStatuses([number], success = self.onStatus)
|
||||
if image_hash == "" or image_hash is None:
|
||||
self.requestVCard(number)
|
||||
self.updateSpectrum(buddy)
|
||||
return buddy
|
||||
|
||||
def updateSpectrum(self, buddy):
|
||||
if buddy.presence == 0:
|
||||
status = Spectrum2.protocol_pb2.STATUS_NONE
|
||||
elif buddy.presence == 'unavailable':
|
||||
status = Spectrum2.protocol_pb2.STATUS_AWAY
|
||||
else:
|
||||
status = Spectrum2.protocol_pb2.STATUS_ONLINE
|
||||
|
||||
statusmsg = buddy.statusMsg
|
||||
if buddy.lastseen != 0:
|
||||
timestamp = time.localtime(buddy.lastseen)
|
||||
statusmsg += time.strftime("\n Last seen: %a, %d %b %Y %H:%M:%S", timestamp)
|
||||
|
||||
iconHash = buddy.image_hash if buddy.image_hash is not None else ""
|
||||
|
||||
self.logger.debug("Updating buddy %s (%s) in %s, image_hash = %s" %
|
||||
(buddy.nick, buddy.number, buddy.groups, iconHash))
|
||||
self.logger.debug("Status Message: %s" % statusmsg)
|
||||
self.backend.handleBuddyChanged(self.user, buddy.number, buddy.nick,
|
||||
buddy.groups, status, statusMessage=statusmsg, iconHash=iconHash)
|
||||
|
||||
def remove(self, number):
|
||||
try:
|
||||
buddy = self[number]
|
||||
del self[number]
|
||||
self.backend.handleBuddyChanged(self.user, number, "", [],
|
||||
Spectrum2.protocol_pb2.STATUS_NONE)
|
||||
self.backend.handleBuddyRemoved(self.user, number)
|
||||
self.session.unsubscribePresence(number)
|
||||
# TODO Sync remove
|
||||
return buddy
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def requestVCard(self, buddy, ID=None):
|
||||
if "/" in buddy:
|
||||
room, nick = buddy.split("/")
|
||||
group = self.session.groups[room]
|
||||
buddynr = None
|
||||
for othernumber, othernick in group.participants.iteritems():
|
||||
if othernick == nick:
|
||||
buddynr = othernumber
|
||||
break
|
||||
if buddynr is None:
|
||||
return
|
||||
else:
|
||||
buddynr = buddy
|
||||
|
||||
|
||||
if buddynr == self.user or buddynr == self.user.split('@')[0]:
|
||||
buddynr = self.session.legacyName
|
||||
|
||||
# Get profile picture
|
||||
self.logger.debug('Requesting profile picture of %s' % buddynr)
|
||||
response = deferred.Deferred()
|
||||
# Error probably means image doesn't exist
|
||||
error = deferred.Deferred()
|
||||
self.session.requestProfilePicture(buddynr, onSuccess=response.run,
|
||||
onFailure=error.run)
|
||||
response = response.arg(0)
|
||||
|
||||
pictureData = response.pictureData()
|
||||
# Send VCard
|
||||
if ID != None:
|
||||
deferred.call(self.logger.debug, 'Sending VCard (%s) with image id %s: %s' %
|
||||
(ID, response.pictureId(), pictureData.then(base64.b64encode)))
|
||||
deferred.call(self.backend.handleVCard, self.user, ID, buddy, "", "",
|
||||
pictureData)
|
||||
# If error
|
||||
error.when(self.logger.debug, 'Sending VCard (%s) without image' % ID)
|
||||
error.when(self.backend.handleVCard, self.user, ID, buddy, "", "", "")
|
||||
|
||||
# Send image hash
|
||||
if not buddynr == self.session.legacyName:
|
||||
try:
|
||||
obuddy = self[buddynr]
|
||||
nick = obuddy.nick
|
||||
groups = obuddy.groups
|
||||
except KeyError:
|
||||
nick = ""
|
||||
groups = []
|
||||
|
||||
def sha1hash(data):
|
||||
hashlib.sha1(data).hexdigest()
|
||||
|
||||
image_hash = pictureData.then(sha1hash)
|
||||
|
||||
deferred.call(self.logger.debug, 'Image hash is %s' % image_hash)
|
||||
deferred.call(self.update, buddynr, nick, groups, image_hash)
|
||||
# No image
|
||||
error.when(self.logger.debug, 'No image')
|
||||
error.when(self.update, buddynr, nick, groups, '')
|
||||
|
||||
def refresh(self, number):
|
||||
self.session.unsubscribePresence(number)
|
||||
self.session.subscribePresence(number)
|
||||
self.requestVCard(number)
|
||||
self.session.requestStatuses([number], success = self.onStatus)
|
|
@ -0,0 +1,139 @@
|
|||
from functools import partial
|
||||
|
||||
class Deferred(object):
|
||||
"""
|
||||
Represents a delayed computation. This is a more elegant way to deal with
|
||||
callbacks.
|
||||
|
||||
A Deferred object can be thought of as a computation whose value is yet to
|
||||
be determined. We can manipulate the Deferred as if it where a regular
|
||||
value by using the then method. Computations dependent on the Deferred will
|
||||
only proceed when the run method is called.
|
||||
|
||||
Attributes of a Deferred can be accessed directly as methods. The result of
|
||||
calling these functions will be Deferred.
|
||||
|
||||
Example:
|
||||
image = Deferred()
|
||||
getImageWithCallback(image.run)
|
||||
image.then(displayFunc)
|
||||
|
||||
colors = Deferred()
|
||||
colors.append('blue')
|
||||
colors.then(print)
|
||||
colors.run(['red', 'green']) #=> ['red', 'green', 'blue']
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.subscribers = []
|
||||
self.computed = False
|
||||
self.args = None
|
||||
self.kwargs = None
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
"""
|
||||
Give a value to the deferred. Calling this method more than once will
|
||||
result in a DeferredHasValue exception to be raised.
|
||||
"""
|
||||
if self.computed:
|
||||
raise DeferredHasValue("Deferred object already has a value.")
|
||||
else:
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
for func, deferred in self.subscribers:
|
||||
deferred.run(func(*args, **kwargs))
|
||||
self.computed = True
|
||||
|
||||
def then(self, func):
|
||||
"""
|
||||
Apply func to Deferred value. Returns a Deferred whose value will be
|
||||
the result of applying func.
|
||||
"""
|
||||
result = Deferred()
|
||||
if self.computed:
|
||||
result.run(func(*self.args, **self.kwargs))
|
||||
else:
|
||||
self.subscribers.append((func, result))
|
||||
return result
|
||||
|
||||
def arg(self, n):
|
||||
"""
|
||||
Returns the nth positional argument of a deferred as a deferred
|
||||
|
||||
Args:
|
||||
n - the index of the positional argument
|
||||
"""
|
||||
def helper(*args, **kwargs):
|
||||
return args[n]
|
||||
return self.then(helper)
|
||||
|
||||
def when(self, func, *args, **kwargs):
|
||||
""" Calls when func(*args, **kwargs) when deferred gets a value """
|
||||
def helper(*args2, **kwargs2):
|
||||
func(*args, **kwargs)
|
||||
return self.then(helper)
|
||||
|
||||
def __getattr__(self, method_name):
|
||||
return getattr(Then(self), method_name)
|
||||
|
||||
|
||||
class Then(object):
|
||||
"""
|
||||
Allows you to call methods on a Deferred.
|
||||
|
||||
Example:
|
||||
colors = Deferred()
|
||||
Then(colors).append('blue')
|
||||
colors.run(['red', 'green'])
|
||||
colors.then(print) #=> ['red', 'green', 'blue']
|
||||
"""
|
||||
def __init__(self, deferred):
|
||||
self.deferred = deferred
|
||||
|
||||
def __getattr__(self, name):
|
||||
def tryCall(obj, *args, **kwargs):
|
||||
if callable(obj):
|
||||
return obj(*args, **kwargs)
|
||||
else:
|
||||
return obj
|
||||
def helper(*args, **kwargs):
|
||||
func = (lambda x: tryCall(getattr(x, name), *args, **kwargs))
|
||||
return self.deferred.then(func)
|
||||
return helper
|
||||
|
||||
def call(func, *args, **kwargs):
|
||||
"""
|
||||
Call a function with deferred arguments
|
||||
|
||||
Example:
|
||||
colors = Deferred()
|
||||
colors.append('blue')
|
||||
colors.run(['red', 'green'])
|
||||
call(print, colors) #=> ['red', 'green', 'blue']
|
||||
call(print, 'hi', colors) #=> hi ['red', 'green', 'blue']
|
||||
"""
|
||||
for i, c in enumerate(args):
|
||||
if isinstance(c, Deferred):
|
||||
# Function without deferred arguments
|
||||
normalfunc = partial(func, *args[:i])
|
||||
# Function with deferred and possibly deferred arguments
|
||||
def restfunc(*arg2, **kwarg2):
|
||||
apply_deferred = partial(normalfunc, *arg2, **kwarg2)
|
||||
return call(apply_deferred, *args[i + 1:], **kwargs)
|
||||
return c.then(restfunc)
|
||||
items = kwargs.items()
|
||||
for i, (k, v) in enumerate(items):
|
||||
if isinstance(v, Deferred):
|
||||
# Function without deferred arguments
|
||||
normalfunc = partial(func, *args, **dict(items[:i]))
|
||||
# Function with deferred and possibly deferred arguments
|
||||
def restfunc2(*arg2, **kwarg2):
|
||||
apply_deferred = partial(normalfunc, *arg2, **kwarg2)
|
||||
return call(apply_deferred, **dict(items[i + 1:]))
|
||||
return v.then(restfunc2)
|
||||
# No items deferred
|
||||
return func(*args, **kwargs)
|
||||
|
||||
class DeferredHasValue(Exception):
|
||||
def __init__(self, string):
|
||||
super(DeferredHasValue, self).__init__(string)
|
|
@ -0,0 +1,84 @@
|
|||
import Spectrum2
|
||||
|
||||
class Group():
|
||||
|
||||
def __init__(self, id, owner, subject, subjectOwner, backend, user):
|
||||
self.id = id
|
||||
self.subject = subject
|
||||
self.subjectOwner = subjectOwner
|
||||
self.owner = owner
|
||||
self.joined = False
|
||||
self.backend = backend
|
||||
self.user = user
|
||||
|
||||
self.nick = "me"
|
||||
# Participants is a number -> nickname dict
|
||||
self.participants = {}
|
||||
|
||||
def addParticipants(self, participants, buddies, yourNumber):
|
||||
"""
|
||||
Adds participants to the group.
|
||||
|
||||
Args:
|
||||
- participants: (Iterable) phone numbers of participants
|
||||
- buddies: (dict) Used to get the nicknames of the participants
|
||||
- yourNumber: The number you are using
|
||||
"""
|
||||
for jid in participants:
|
||||
number = jid.split('@')[0]
|
||||
try:
|
||||
nick = buddies[number].nick
|
||||
except KeyError:
|
||||
nick = number
|
||||
if number == yourNumber:
|
||||
nick = self.nick
|
||||
if nick == "":
|
||||
nick = number
|
||||
self.participants[number] = nick
|
||||
|
||||
def sendParticipantsToSpectrum(self, yourNumber):
|
||||
for number, nick in self.participants.iteritems():
|
||||
if number == self.owner:
|
||||
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_MODERATOR
|
||||
else:
|
||||
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE
|
||||
if number == yourNumber:
|
||||
flags = flags | Spectrum2.protocol_pb2.PARTICIPANT_FLAG_ME
|
||||
|
||||
try:
|
||||
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_ONLINE,
|
||||
self.backend.sessions[self.user].buddies[number].image_hash)
|
||||
except KeyError:
|
||||
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_ONLINE)
|
||||
|
||||
def removeParticipants(self, participants):
|
||||
for jid in participants:
|
||||
number = jid.split('@')[0]
|
||||
nick = self.participants[number]
|
||||
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE
|
||||
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_NONE)
|
||||
del self.participants[number]
|
||||
|
||||
def leaveRoom(self):
|
||||
for number in self.participants:
|
||||
nick = self.participants[number]
|
||||
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_ROOM_NOT_FOUND
|
||||
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_NONE)
|
||||
|
||||
def changeNick(self, number, new_nick):
|
||||
if self.participants[number] == new_nick:
|
||||
return
|
||||
if number == self.owner:
|
||||
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_MODERATOR
|
||||
else:
|
||||
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE
|
||||
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_ONLINE, new_nick)
|
||||
self.participants[number] = new_nick
|
||||
|
||||
def _updateParticipant(self, number, flags, status, imageHash = "", newNick = ""):
|
||||
nick = self.participants[number]
|
||||
# Notice the status message is the buddy's number
|
||||
if self.joined:
|
||||
self.backend.handleParticipantChanged(
|
||||
self.user, nick, self.id, flags,
|
||||
status, number, newname = newNick, iconHash = imageHash)
|
|
@ -0,0 +1,149 @@
|
|||
import sys
|
||||
import logging
|
||||
import Spectrum2
|
||||
|
||||
from .yowsupwrapper import YowsupApp
|
||||
|
||||
from . import threadutils
|
||||
|
||||
|
||||
class RegisterSession(YowsupApp):
|
||||
"""
|
||||
A dummy Session object that is used to register a user to whatsapp
|
||||
"""
|
||||
WANT_CC = 0
|
||||
WANT_SMS = 1
|
||||
def __init__(self, backend, user, legacyName, extra):
|
||||
self.user = user
|
||||
self.number = legacyName
|
||||
self.backend = backend
|
||||
self.countryCode = ''
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.state = self.WANT_CC
|
||||
|
||||
def login(self, password=""):
|
||||
self.backend.handleConnected(self.user)
|
||||
self.backend.handleBuddyChanged(self.user, 'bot', 'bot',
|
||||
['Admin'], Spectrum2.protocol_pb2.STATUS_ONLINE)
|
||||
self.backend.handleMessage(self.user, 'bot',
|
||||
'Please enter your country code')
|
||||
|
||||
def sendMessageToWA(self, buddy, message, ID='', xhtml=''):
|
||||
if buddy == 'bot' and self.state == self.WANT_CC:
|
||||
try:
|
||||
country_code = int(message.strip())
|
||||
except ValueError:
|
||||
self.backend.handleMessage(self.user, 'bot',
|
||||
'Country code must be a number')
|
||||
else: # Succeded in decoding country code
|
||||
country_code = "%s" % country_code
|
||||
if country_code != self.number[:len(country_code)]:
|
||||
self.backend.handleMessage(self.user,
|
||||
'bot', 'Number does not start with provided country code')
|
||||
else:
|
||||
self.backend.handleMessage(self.user, 'bot', 'Requesting sms code')
|
||||
self.logger.debug('Requesting SMS code for %s' % self.user)
|
||||
self.countryCode = country_code
|
||||
self._requestSMSCodeNonBlock()
|
||||
elif buddy == 'bot' and self.state == self.WANT_SMS:
|
||||
code = message.strip()
|
||||
if self._checkSMSFormat(code):
|
||||
self._requestPassword(code)
|
||||
else:
|
||||
self.backend.handleMessage(self.user,
|
||||
'bot', 'Invalid code. Must be of the form XXX-XXX.')
|
||||
else:
|
||||
self.logger.warn('Unauthorised user (%s) attempting to send messages' %
|
||||
self.user)
|
||||
self.backend.handleMessage(self.user, buddy,
|
||||
'You are not logged in yet. You can only send messages to bot.')
|
||||
|
||||
def _checkSMSFormat(self, sms):
|
||||
splitting = sms.split('-')
|
||||
if len(splitting) != 2:
|
||||
return False
|
||||
a, b = splitting
|
||||
if len(a) != 3 and len(b) != 3:
|
||||
return False
|
||||
try:
|
||||
int(a)
|
||||
int(b)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _requestSMSCodeNonBlock(self):
|
||||
number = self.number[len(self.countryCode):]
|
||||
threadFunc = lambda: self.requestSMSCode(self.countryCode, number)
|
||||
threadutils.runInThread(threadFunc, self._confirmation)
|
||||
self.backend.handleMessage(self.user, 'bot', 'SMS Code Sent')
|
||||
|
||||
def _confirmation(self, result):
|
||||
self.state = self.WANT_SMS
|
||||
resultStr = self._resultToString(result)
|
||||
self.backend.handleMessage(self.user, 'bot', 'Response:')
|
||||
self.backend.handleMessage(self.user, 'bot', resultStr)
|
||||
self.backend.handleMessage(self.user, 'bot', 'Please enter SMS Code')
|
||||
|
||||
def _requestPassword(self, smsCode):
|
||||
cc = self.countryCode
|
||||
number = self.number[len(cc):]
|
||||
threadFunc = lambda: self.requestPassword(cc, number, smsCode)
|
||||
threadutils.runInThread(threadFunc, self._gotPassword)
|
||||
self.backend.handleMessage(self.user, 'bot', 'Getting Password')
|
||||
|
||||
def _gotPassword(self, result):
|
||||
resultStr = self._resultToString(result)
|
||||
self.backend.handleMessage(self.user, 'bot', 'Response:')
|
||||
self.backend.handleMessage(self.user, 'bot', resultStr)
|
||||
self.backend.handleMessage(self.user, 'bot', 'Logging you in')
|
||||
password = result['pw']
|
||||
self.backend.relogin(self.user, self.number, password, None)
|
||||
|
||||
def _resultToString(self, result):
|
||||
unistr = str if sys.version_info >= (3, 0) else unicode
|
||||
out = []
|
||||
for k, v in result.items():
|
||||
if v is None:
|
||||
continue
|
||||
out.append("%s: %s" % (k, v))
|
||||
|
||||
return "\n".join(out)
|
||||
|
||||
# Dummy methods. Whatsapp backend might call these, but they should have no
|
||||
# effect
|
||||
def logout(self):
|
||||
pass
|
||||
|
||||
def joinRoom(self, room, nickname):
|
||||
pass
|
||||
|
||||
def leaveRoom(self, room):
|
||||
pass
|
||||
|
||||
def changeStatusMessage(self, statusMessage):
|
||||
pass
|
||||
|
||||
def changeStatus(self, status):
|
||||
pass
|
||||
|
||||
def loadBuddies(self, buddies):
|
||||
pass
|
||||
|
||||
def updateBuddy(self, buddies):
|
||||
pass
|
||||
|
||||
def removeBuddy(self, buddies):
|
||||
pass
|
||||
|
||||
def sendTypingStarted(self, buddy):
|
||||
pass
|
||||
|
||||
def sendTypingStopped(self, buddy):
|
||||
pass
|
||||
|
||||
def requestVCard(self, buddy, ID):
|
||||
pass
|
||||
|
||||
def setProfilePicture(self, previewPicture, fullPicture = None):
|
||||
pass
|
|
@ -0,0 +1,838 @@
|
|||
import logging
|
||||
import urllib
|
||||
import time
|
||||
import os
|
||||
|
||||
from yowsup.layers.protocol_media.mediauploader import MediaUploader
|
||||
from yowsup.layers.protocol_media.mediadownloader import MediaDownloader
|
||||
|
||||
import Spectrum2
|
||||
|
||||
from . import deferred
|
||||
from .buddy import BuddyList
|
||||
from .group import Group
|
||||
from .bot import Bot
|
||||
from .yowsupwrapper import YowsupApp
|
||||
|
||||
def ago(secs):
|
||||
periods = ["second", "minute", "hour", "day", "week", "month", "year", "decade"]
|
||||
lengths = [60, 60, 24, 7,4.35, 12, 10]
|
||||
|
||||
j = 0
|
||||
diff = secs
|
||||
|
||||
while diff >= lengths[j]:
|
||||
diff /= lengths[j]
|
||||
diff = round(diff)
|
||||
j += 1
|
||||
|
||||
period = periods[j]
|
||||
if diff > 1: period += "s"
|
||||
|
||||
return "%d %s ago" % (diff, period)
|
||||
|
||||
|
||||
class MsgIDs:
|
||||
def __init__(self, xmppId, waId):
|
||||
self.xmppId = xmppId
|
||||
self.waId = waId
|
||||
self.cnt = 0
|
||||
|
||||
class Session(YowsupApp):
|
||||
broadcast_prefix = '\U0001F4E2 '
|
||||
|
||||
def __init__(self, backend, user, legacyName, extra):
|
||||
super(Session, self).__init__()
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.logger.info("Created: %s" % legacyName)
|
||||
|
||||
self.backend = backend
|
||||
self.user = user
|
||||
self.legacyName = legacyName
|
||||
|
||||
self.status = Spectrum2.protocol_pb2.STATUS_NONE
|
||||
self.statusMessage = ''
|
||||
|
||||
self.groups = {}
|
||||
self.gotGroupList = False
|
||||
# Functions to exectute when logged in via yowsup
|
||||
self.loginQueue = []
|
||||
self.joinRoomQueue = []
|
||||
self.presenceRequested = []
|
||||
self.offlineQueue = []
|
||||
self.msgIDs = { }
|
||||
self.groupOfflineQueue = { }
|
||||
self.loggedIn = False
|
||||
self.recvMsgIDs = []
|
||||
|
||||
self.timer = None
|
||||
self.password = None
|
||||
self.initialized = False
|
||||
self.lastMsgId = None
|
||||
self.synced = False
|
||||
|
||||
self.buddies = BuddyList(self.legacyName, self.backend, self.user, self)
|
||||
self.bot = Bot(self)
|
||||
|
||||
self.imgMsgId = None
|
||||
self.imgPath = ""
|
||||
self.imgBuddy = None
|
||||
self.imgType = ""
|
||||
|
||||
|
||||
def __del__(self): # handleLogoutRequest
|
||||
self.logout()
|
||||
|
||||
def logout(self):
|
||||
self.logger.info("%s logged out" % self.user)
|
||||
super(Session, self).logout()
|
||||
self.loggedIn = False
|
||||
|
||||
def login(self, password):
|
||||
self.logger.info("%s attempting login" % self.user)
|
||||
self.password = password
|
||||
self.shouldBeConncted = True
|
||||
super(Session, self).login(self.legacyName, self.password)
|
||||
|
||||
def _shortenGroupId(self, gid):
|
||||
# FIXME: might have problems if number begins with 0
|
||||
return gid
|
||||
# return '-'.join(hex(int(s))[2:] for s in gid.split('-'))
|
||||
|
||||
def _lengthenGroupId(self, gid):
|
||||
return gid
|
||||
# FIXME: might have problems if number begins with 0
|
||||
# return '-'.join(str(int(s, 16)) for s in gid.split('-'))
|
||||
|
||||
def updateRoomList(self):
|
||||
rooms = []
|
||||
text = []
|
||||
for room, group in self.groups.iteritems():
|
||||
rooms.append([self._shortenGroupId(room), group.subject])
|
||||
text.append(self._shortenGroupId(room) + '@' + self.backend.spectrum_jid + ' :' + group.subject)
|
||||
|
||||
self.logger.debug("Got rooms: %s" % rooms)
|
||||
self.backend.handleRoomList(rooms)
|
||||
message = "Note, you are a participant of the following groups:\n" + \
|
||||
"\n".join(text) + "\nIf you do not join them you will lose messages"
|
||||
#self.bot.send(message)
|
||||
|
||||
def _updateGroups(self, response, _):
|
||||
self.logger.debug('Received groups list %s' % response)
|
||||
groups = response.getGroups()
|
||||
for group in groups:
|
||||
room = group.getId()
|
||||
# ensure self.groups[room] exists
|
||||
if room not in self.groups:
|
||||
owner = group.getOwner().split('@')[0]
|
||||
subjectOwner = group.getSubjectOwner().split('@')[0]
|
||||
subject = group.getSubject()
|
||||
self.groups[room] = Group(room, owner, subject, subjectOwner,
|
||||
self.backend, self.user)
|
||||
# add/update room participants
|
||||
self.groups[room].addParticipants(group.getParticipants().keys(),
|
||||
self.buddies, self.legacyName)
|
||||
self.gotGroupList = True
|
||||
# join rooms
|
||||
while self.joinRoomQueue:
|
||||
self.joinRoom(*self.joinRoomQueue.pop(0))
|
||||
# deliver queued offline messages
|
||||
for room in self.groupOfflineQueue:
|
||||
while self.groupOfflineQueue[room]:
|
||||
msg = self.groupOfflineQueue[room].pop(0)
|
||||
self.backend.handleMessage(self.user, room, msg[1], msg[0], "",
|
||||
msg[2])
|
||||
self.logger.debug("Send queued group message to: %s %s %s" %
|
||||
(msg[0], msg[1], msg[2]))
|
||||
# pass update to backend
|
||||
self.updateRoomList()
|
||||
|
||||
def joinRoom(self, room, nick):
|
||||
if not self.gotGroupList:
|
||||
self.joinRoomQueue.append((room, nick))
|
||||
return
|
||||
room = self._lengthenGroupId(room)
|
||||
if room in self.groups:
|
||||
self.logger.info("Joining room: %s room=%s, nick=%s" %
|
||||
(self.legacyName, room, nick))
|
||||
|
||||
group = self.groups[room]
|
||||
group.joined = True
|
||||
group.nick = nick
|
||||
group.participants[self.legacyName] = nick
|
||||
try:
|
||||
ownerNick = group.participants[group.subjectOwner]
|
||||
except KeyError:
|
||||
ownerNick = group.subjectOwner
|
||||
|
||||
group.sendParticipantsToSpectrum(self.legacyName)
|
||||
self.backend.handleSubject(self.user, self._shortenGroupId(room),
|
||||
group.subject, ownerNick)
|
||||
self.logger.debug("Room subject: room=%s, subject=%s" %
|
||||
(room, group.subject))
|
||||
self.backend.handleRoomNicknameChanged(
|
||||
self.user, self._shortenGroupId(room), group.subject
|
||||
)
|
||||
else:
|
||||
self.logger.warn("Room doesn't exist: %s" % room)
|
||||
|
||||
def leaveRoom(self, room):
|
||||
if room in self.groups:
|
||||
self.logger.info("Leaving room: %s room=%s" % (self.legacyName, room))
|
||||
group = self.groups[room]
|
||||
group.joined = False
|
||||
else:
|
||||
self.logger.warn("Room doesn't exist: %s. Unable to leave." % room)
|
||||
|
||||
def _lastSeen(self, number, seconds):
|
||||
self.logger.debug("Last seen %s at %s seconds" % (number, seconds))
|
||||
if seconds < 60:
|
||||
self.onPresenceAvailable(number)
|
||||
else:
|
||||
self.onPresenceUnavailable(number)
|
||||
def sendReadReceipts(self, buddy):
|
||||
for _id, _from, participant, t in self.recvMsgIDs:
|
||||
if _from.split('@')[0] == buddy:
|
||||
self.sendReceipt(_id, _from, 'read', participant)
|
||||
self.recvMsgIDs.remove((_id, _from, participant, t))
|
||||
self.logger.debug("Send read receipt to %s (ID: %s)", _from, _id)
|
||||
|
||||
# Called by superclass
|
||||
def onAuthSuccess(self, status, kind, creation,
|
||||
expiration, props, nonce, t):
|
||||
self.logger.info("Auth success: %s" % self.user)
|
||||
|
||||
self.backend.handleConnected(self.user)
|
||||
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name,
|
||||
["Admin"], Spectrum2.protocol_pb2.STATUS_ONLINE)
|
||||
# Initialisation?
|
||||
self.requestPrivacyList()
|
||||
self.requestClientConfig()
|
||||
self.requestServerProperties()
|
||||
# ?
|
||||
|
||||
self.logger.debug('Requesting groups list')
|
||||
self.requestGroupsList(self._updateGroups)
|
||||
# self.requestBroadcastList()
|
||||
|
||||
# This should handle, sync, statuses, and presence
|
||||
self.sendPresence(True)
|
||||
for func in self.loginQueue:
|
||||
func()
|
||||
|
||||
if self.initialized == False:
|
||||
self.sendOfflineMessages()
|
||||
#self.bot.call("welcome")
|
||||
self.initialized = True
|
||||
|
||||
self.loggedIn = True
|
||||
|
||||
# Called by superclass
|
||||
def onAuthFailed(self, reason):
|
||||
self.logger.info("Auth failed: %s (%s)" % (self.user, reason))
|
||||
self.backend.handleDisconnected(self.user, 0, reason)
|
||||
self.password = None
|
||||
self.loggedIn = False
|
||||
|
||||
# Called by superclass
|
||||
def onDisconnect(self):
|
||||
self.logger.debug('Disconnected')
|
||||
self.backend.handleDisconnected(self.user, 0, 'Disconnected for unknown reasons')
|
||||
|
||||
# Called by superclass
|
||||
def onReceipt(self, _id, _from, timestamp, type, participant, offline, items):
|
||||
self.logger.debug("received receipt, sending ack: %s" %
|
||||
[ _id, _from, timestamp, type, participant, offline, items ]
|
||||
)
|
||||
try:
|
||||
number = _from.split('@')[0]
|
||||
self.backend.handleMessageAck(self.user, number, self.msgIDs[_id].xmppId)
|
||||
self.msgIDs[_id].cnt = self.msgIDs[_id].cnt + 1
|
||||
if self.msgIDs[_id].cnt == 2:
|
||||
del self.msgIDs[_id]
|
||||
except KeyError:
|
||||
self.logger.error("Message %s not found. Unable to send ack" % _id)
|
||||
|
||||
# Called by superclass
|
||||
def onAck(self, _id, _class, _from, timestamp):
|
||||
self.logger.debug('received ack: %s' % [ _id, _class, _from, timestamp ])
|
||||
|
||||
# Called by superclass
|
||||
def onTextMessage(self, _id, _from, to, notify, timestamp, participant,
|
||||
offline, retry, body):
|
||||
buddy = _from.split('@')[0]
|
||||
messageContent = body
|
||||
self.sendReceipt(_id, _from, None, participant)
|
||||
self.recvMsgIDs.append((_id, _from, participant, timestamp))
|
||||
self.logger.info("Message received from %s to %s: %s (at ts=%s)" %
|
||||
(buddy, self.legacyName, messageContent, timestamp))
|
||||
|
||||
if participant is not None: # Group message or broadcast
|
||||
partname = participant.split('@')[0]
|
||||
if _from.split('@')[1] == 'broadcast': # Broadcast message
|
||||
message = self.broadcast_prefix + messageContent
|
||||
self.sendMessageToXMPP(partname, message, timestamp)
|
||||
else: # Group message
|
||||
if notify is None:
|
||||
notify = ""
|
||||
self.sendGroupMessageToXMPP(buddy, partname, messageContent,
|
||||
timestamp, notify)
|
||||
else:
|
||||
self.sendMessageToXMPP(buddy, messageContent, timestamp)
|
||||
|
||||
# Called by superclass
|
||||
def onImage(self, image):
|
||||
if image.caption is None:
|
||||
image.caption = ''
|
||||
|
||||
self.onMedia(image, "image")
|
||||
|
||||
|
||||
# Called by superclass
|
||||
def onAudio(self, audio):
|
||||
self.onMedia(audio, "audio")
|
||||
|
||||
|
||||
# Called by superclass
|
||||
def onVideo(self, video):
|
||||
self.onMedia(video, "video")
|
||||
|
||||
|
||||
def onMedia(self, media, type):
|
||||
self.logger.debug('Received %s message: %s' % (type, media))
|
||||
buddy = media._from.split('@')[0]
|
||||
participant = media.participant
|
||||
caption = ''
|
||||
|
||||
if media.isEncrypted():
|
||||
self.logger.debug('Received encrypted media message')
|
||||
if self.backend.specConf is not None and self.backend.specConf.__getitem__("service.web_directory") is not None and self.backend.specConf.__getitem__("service.web_url") is not None :
|
||||
ipath = "/" + str(media.timestamp) + media.getExtension()
|
||||
|
||||
with open(self.backend.specConf.__getitem__("service.web_directory") + ipath,"wb") as f:
|
||||
f.write(media.getMediaContent())
|
||||
url = self.backend.specConf.__getitem__("service.web_url") + ipath
|
||||
else:
|
||||
self.logger.warn('Received encrypted media: web storage not set in config!')
|
||||
url = media.url
|
||||
|
||||
else:
|
||||
url = media.url
|
||||
|
||||
if type == 'image':
|
||||
caption = media.caption
|
||||
|
||||
if participant is not None: # Group message
|
||||
partname = participant.split('@')[0]
|
||||
if media._from.split('@')[1] == 'broadcast': # Broadcast message
|
||||
self.sendMessageToXMPP(partname, self.broadcast_prefix, media.timestamp)
|
||||
self.sendMessageToXMPP(partname, url, media.timestamp)
|
||||
self.sendMessageToXMPP(partname, caption, media.timestamp)
|
||||
else: # Group message
|
||||
self.sendGroupMessageToXMPP(buddy, partname, url, media.timestamp)
|
||||
self.sendGroupMessageToXMPP(buddy, partname, caption, media.timestamp)
|
||||
else:
|
||||
self.sendMessageToXMPP(buddy, url, media.timestamp)
|
||||
self.sendMessageToXMPP(buddy, caption, media.timestamp)
|
||||
|
||||
self.sendReceipt(media._id, media._from, None, media.participant)
|
||||
self.recvMsgIDs.append((media._id, media._from, media.participant, media.timestamp))
|
||||
|
||||
def onLocation(self, location):
|
||||
buddy = location._from.split('@')[0]
|
||||
latitude = location.getLatitude()
|
||||
longitude = location.getLongitude()
|
||||
url = location.getLocationURL()
|
||||
participant = location.participant
|
||||
latlong = 'geo:' + latitude + ',' + longitude
|
||||
|
||||
self.logger.debug("Location received from %s: %s, %s", (buddy, latitude, longitude))
|
||||
|
||||
if participant is not None: # Group message
|
||||
partname = participant.split('@')[0]
|
||||
if location._from.split('@')[1] == 'broadcast': # Broadcast message
|
||||
self.sendMessageToXMPP(partname, self.broadcast_prefix, location.timestamp)
|
||||
if url is not None:
|
||||
self.sendMessageToXMPP(partname, url, location.timestamp)
|
||||
self.sendMessageToXMPP(partname, latlong, location.timestamp)
|
||||
else: # Group message
|
||||
if url is not None:
|
||||
self.sendGroupMessageToXMPP(buddy, partname, url, location.timestamp)
|
||||
self.sendGroupMessageToXMPP(buddy, partname, latlong, location.timestamp)
|
||||
else:
|
||||
if url is not None:
|
||||
self.sendMessageToXMPP(buddy, url, location.timestamp)
|
||||
self.sendMessageToXMPP(buddy, latlong, location.timestamp)
|
||||
self.sendReceipt(location._id, location._from, None, location.participant)
|
||||
self.recvMsgIDs.append((location._id, location._from, location.participant, location.timestamp))
|
||||
|
||||
|
||||
|
||||
# Called by superclass
|
||||
def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant):
|
||||
self.logger.debug('received VCard: %s' %
|
||||
[ _id, _from, name, card_data, to, notify, timestamp, participant ]
|
||||
)
|
||||
message = "Received VCard (not implemented yet)"
|
||||
buddy = _from.split("@")[0]
|
||||
if participant is not None: # Group message
|
||||
partname = participant.split('@')[0]
|
||||
if _from.split('@')[1] == 'broadcast': # Broadcast message
|
||||
message = self.broadcast_prefix + message
|
||||
self.sendMessageToXMPP(partname, message, timestamp)
|
||||
else: # Group message
|
||||
self.sendGroupMessageToXMPP(buddy, partname, message, timestamp)
|
||||
else:
|
||||
self.sendMessageToXMPP(buddy, message, timestamp)
|
||||
# self.sendMessageToXMPP(buddy, card_data)
|
||||
#self.transferFile(buddy, str(name), card_data)
|
||||
self.sendReceipt(_id, _from, None, participant)
|
||||
self.recvMsgIDs.append((_id, _from, participant, timestamp))
|
||||
|
||||
|
||||
def transferFile(self, buddy, name, data):
|
||||
# Not working
|
||||
self.logger.debug('transfering file: %s' % name)
|
||||
self.backend.handleFTStart(self.user, buddy, name, len(data))
|
||||
self.backend.handleFTData(0, data)
|
||||
self.backend.handleFTFinish(self.user, buddy, name, len(data), 0)
|
||||
|
||||
# Called by superclass
|
||||
def onContactTyping(self, buddy):
|
||||
self.logger.info("Started typing: %s" % buddy)
|
||||
if buddy != 'bot':
|
||||
self.sendPresence(True)
|
||||
self.backend.handleBuddyTyping(self.user, buddy)
|
||||
|
||||
if self.timer != None:
|
||||
self.timer.cancel()
|
||||
|
||||
# Called by superclass
|
||||
def onContactPaused(self, buddy):
|
||||
self.logger.info("Paused typing: %s" % buddy)
|
||||
if buddy != 'bot':
|
||||
self.backend.handleBuddyTyped(self.user, buddy)
|
||||
self.timer = threading.Timer(3, self.backend.handleBuddyStoppedTyping,
|
||||
(self.user, buddy)).start()
|
||||
|
||||
# Called by superclass
|
||||
def onAddedToGroup(self, group):
|
||||
self.logger.debug("Added to group: %s" % group)
|
||||
room = group.getGroupId()
|
||||
owner = group.getCreatorJid(full = False)
|
||||
subjectOwner = group.getSubjectOwnerJid(full = False)
|
||||
subject = group.getSubject()
|
||||
|
||||
self.groups[room] = Group(room, owner, subject, subjectOwner, self.backend, self.user)
|
||||
self.groups[room].addParticipants(group.getParticipants(), self.buddies, self.legacyName)
|
||||
self.bot.send("You have been added to group: %s@%s (%s)"
|
||||
% (self._shortenGroupId(room), subject, self.backend.spectrum_jid))
|
||||
|
||||
# Called by superclass
|
||||
def onParticipantsAddedToGroup(self, group):
|
||||
self.logger.debug("Participants added to group: %s" % group)
|
||||
room = group.getGroupId().split('@')[0]
|
||||
self.groups[room].addParticipants(group.getParticipants(), self.buddies, self.legacyName)
|
||||
self.groups[room].sendParticipantsToSpectrum(self.legacyName)
|
||||
|
||||
# Called by superclass
|
||||
def onSubjectChanged(self, room, subject, subjectOwner, timestamp):
|
||||
self.logger.debug(
|
||||
"onSubjectChange(rrom=%s, subject=%s, subjectOwner=%s, timestamp=%s)" %
|
||||
(room, subject, subjectOwner, timestamp)
|
||||
)
|
||||
try:
|
||||
group = self.groups[room]
|
||||
except KeyError:
|
||||
self.logger.error("Subject of non-existant group (%s) changed" % group)
|
||||
else:
|
||||
group.subject = subject
|
||||
group.subjectOwner = subjectOwner
|
||||
if not group.joined:
|
||||
# We have not joined group so we should not send subject
|
||||
return
|
||||
self.backend.handleSubject(self.user, room, subject, subjectOwner)
|
||||
self.backend.handleRoomNicknameChanged(self.user, room, subject)
|
||||
|
||||
# Called by superclass
|
||||
def onParticipantsRemovedFromGroup(self, room, participants):
|
||||
self.logger.debug("Participants removed from group: %s, %s" %
|
||||
(room, participants))
|
||||
self.groups[room].removeParticipants(participants)
|
||||
|
||||
# Called by superclass
|
||||
def onContactStatusChanged(self, number, status):
|
||||
self.logger.debug("%s changed their status to %s" % (number, status))
|
||||
try:
|
||||
buddy = self.buddies[number]
|
||||
buddy.statusMsg = status
|
||||
self.buddies.updateSpectrum(buddy)
|
||||
except KeyError:
|
||||
self.logger.debug("%s not in buddy list" % number)
|
||||
|
||||
# Called by superclass
|
||||
def onContactPictureChanged(self, number):
|
||||
self.logger.debug("%s changed their profile picture" % number)
|
||||
self.buddies.requestVCard(number)
|
||||
|
||||
# Called by superclass
|
||||
def onContactAdded(self, number, nick):
|
||||
self.logger.debug("Adding new contact %s (%s)" % (nick, number))
|
||||
self.updateBuddy(number, nick, [])
|
||||
|
||||
# Called by superclass
|
||||
def onContactRemoved(self, number):
|
||||
self.logger.debug("Removing contact %s" % number)
|
||||
self.removeBuddy(number)
|
||||
|
||||
def onContactUpdated(self, oldnumber, newnumber):
|
||||
self.logger.debug("Contact has changed number from %s to %s" %
|
||||
(oldnumber, newnumber))
|
||||
if newnumber in self.buddies:
|
||||
self.logger.warn("Contact %s exists, just updating" % newnumber)
|
||||
self.buddies.refresh(newnumber)
|
||||
try:
|
||||
buddy = self.buddies[oldnumber]
|
||||
except KeyError:
|
||||
self.logger.warn("Old contact (%s) not found. Adding new contact (%s)" %
|
||||
(oldnumber, newnumber))
|
||||
nick = ""
|
||||
else:
|
||||
self.removeBuddy(buddy.number)
|
||||
nick = buddy.nick
|
||||
self.updateBuddy(newnumber, nick, [])
|
||||
|
||||
def onPresenceReceived(self, _type, name, jid, lastseen):
|
||||
self.logger.info("Presence received: %s %s %s %s" % (_type, name, jid, lastseen))
|
||||
buddy = jid.split("@")[0]
|
||||
try:
|
||||
buddy = self.buddies[buddy]
|
||||
except KeyError:
|
||||
# Sometimes whatsapp send our own presence
|
||||
if buddy != self.legacyName:
|
||||
self.logger.error("Buddy not found: %s" % buddy)
|
||||
return
|
||||
|
||||
if (lastseen == buddy.lastseen) and (_type == buddy.presence):
|
||||
return
|
||||
|
||||
if ((lastseen != "deny") and (lastseen != None) and (lastseen != "none")):
|
||||
buddy.lastseen = int(lastseen)
|
||||
if (_type == None):
|
||||
buddy.lastseen = time.time()
|
||||
|
||||
buddy.presence = _type
|
||||
|
||||
if _type == "unavailable":
|
||||
self.onPresenceUnavailable(buddy)
|
||||
else:
|
||||
self.onPresenceAvailable(buddy)
|
||||
|
||||
def onPresenceAvailable(self, buddy):
|
||||
self.logger.info("Is available: %s" % buddy)
|
||||
self.buddies.updateSpectrum(buddy)
|
||||
|
||||
def onPresenceUnavailable(self, buddy):
|
||||
self.logger.info("Is unavailable: %s" % buddy)
|
||||
self.buddies.updateSpectrum(buddy)
|
||||
|
||||
# spectrum RequestMethods
|
||||
def sendTypingStarted(self, buddy):
|
||||
if buddy != "bot":
|
||||
self.logger.info("Started typing: %s to %s" % (self.legacyName, buddy))
|
||||
self.sendTyping(buddy, True)
|
||||
self.sendReadReceipts(buddy)
|
||||
# If he is typing he is present
|
||||
# I really don't know where else to put this.
|
||||
# Ideally, this should be sent if the user is looking at his client
|
||||
self.sendPresence(True)
|
||||
|
||||
def sendTypingStopped(self, buddy):
|
||||
if buddy != "bot":
|
||||
self.logger.info("Stopped typing: %s to %s" % (self.legacyName, buddy))
|
||||
self.sendTyping(buddy, False)
|
||||
self.sendReadReceipts(buddy)
|
||||
|
||||
def sendImage(self, message, ID, to):
|
||||
if (".jpg" in message.lower()):
|
||||
imgType = "jpg"
|
||||
if (".webp" in message.lower()):
|
||||
imgType = "webp"
|
||||
|
||||
success = deferred.Deferred()
|
||||
error = deferred.Deferred()
|
||||
self.downloadMedia(message, success.run, error.run)
|
||||
|
||||
# Success
|
||||
path = success.arg(0)
|
||||
call(self.logger.info, "Success: Image downloaded to %s" % path)
|
||||
pathWithExt = path.then(lambda p: p + "." + imgType)
|
||||
call(os.rename, path, pathWithExt)
|
||||
pathJpg = path.then(lambda p: p + ".jpg")
|
||||
if imgType != "jpg":
|
||||
im = call(Image.open, pathWithExt)
|
||||
call(im.save, pathJpg)
|
||||
call(os.remove, pathWithExt)
|
||||
call(self.logger.info, "Sending image to %s" % to)
|
||||
waId = deferred.Deferred()
|
||||
call(super(Session, self).sendImage, to, pathJpg, onSuccess = waId.run)
|
||||
call(self.setWaId, ID, waId)
|
||||
waId.when(call, os.remove, pathJpg)
|
||||
waId.when(self.logger.info, "Image sent")
|
||||
|
||||
# Error
|
||||
error.when(self.logger.info, "Download Error. Sending message as is.")
|
||||
waId = error.when(self.sendTextMessage, to, message)
|
||||
call(self.setWaId, ID, waId)
|
||||
|
||||
def setWaId(self, XmppId, waId):
|
||||
self.msgIDs[waId] = MsgIDs(XmppId, waId)
|
||||
|
||||
def sendMessageToWA(self, sender, message, ID, xhtml=""):
|
||||
self.logger.info("Message sent from %s to %s: %s (xhtml=%s)" %
|
||||
(self.legacyName, sender, message, xhtml))
|
||||
|
||||
self.sendReadReceipts(sender)
|
||||
|
||||
if sender == "bot":
|
||||
self.bot.parse(message)
|
||||
elif "-" in sender: # group msg
|
||||
if "/" in sender: # directed at single user
|
||||
room, nick = sender.split("/")
|
||||
group = self.groups[room]
|
||||
number = None
|
||||
for othernumber, othernick in group.participants.iteritems():
|
||||
if othernick == nick:
|
||||
number = othernumber
|
||||
break
|
||||
if number is not None:
|
||||
self.logger.debug("Private message sent from %s to %s" % (self.legacyName, number))
|
||||
waId = self.sendTextMessage(number + '@s.whatsapp.net', message)
|
||||
self.msgIDs[waId] = MsgIDs( ID, waId)
|
||||
else:
|
||||
self.logger.error("Attempted to send private message to non-existent user")
|
||||
self.logger.debug("%s to %s in %s" % (self.legacyName, nick, room))
|
||||
else:
|
||||
room = sender
|
||||
if message[0] == '\\' and message[:1] != '\\\\':
|
||||
self.logger.debug("Executing command %s in %s" % (message, room))
|
||||
self.executeCommand(message, room)
|
||||
else:
|
||||
try:
|
||||
group = self.groups[self._lengthenGroupId(room)]
|
||||
self.logger.debug("Group Message from %s to %s Groups: %s" %
|
||||
(group.nick , group , self.groups))
|
||||
self.backend.handleMessage(
|
||||
self.user, room, message, group.nick, xhtml=xhtml
|
||||
)
|
||||
except KeyError:
|
||||
self.logger.error('Group not found: %s' % room)
|
||||
|
||||
if (".jpg" in message.lower()) or (".webp" in message.lower()):
|
||||
self.sendImage(message, ID, room + '@g.us')
|
||||
elif "geo:" in message.lower():
|
||||
self._sendLocation(room + "@g.us", message, ID)
|
||||
else:
|
||||
self.sendTextMessage(room + '@g.us', message)
|
||||
else: # private msg
|
||||
buddy = sender
|
||||
if message.split(" ").pop(0) == "\\lastseen":
|
||||
self.presenceRequested.append(buddy)
|
||||
self._requestLastSeen(buddy)
|
||||
elif message.split(" ").pop(0) == "\\gpp":
|
||||
self.sendMessageToXMPP(buddy, "Fetching Profile Picture")
|
||||
self.requestVCard(buddy)
|
||||
elif (".jpg" in message.lower()) or (".webp" in message.lower()):
|
||||
self.sendImage(message, ID, buddy + "@s.whatsapp.net")
|
||||
elif "geo:" in message.lower():
|
||||
self._sendLocation(buddy + "@s.whatsapp.net", message, ID)
|
||||
else:
|
||||
waId = self.sendTextMessage(sender + '@s.whatsapp.net', message)
|
||||
self.msgIDs[waId] = MsgIDs(ID, waId)
|
||||
|
||||
# self.logger.info("WA Message send to %s with ID %s", buddy, waId)
|
||||
|
||||
def executeCommand(self, command, room):
|
||||
if command == '\\leave':
|
||||
self.logger.debug("Leaving room %s", room)
|
||||
self.leaveGroup(room) # Leave group on whatsapp side
|
||||
group = self.groups[room]
|
||||
group.leaveRoom() # Delete Room on spectrum side
|
||||
del self.groups[room]
|
||||
|
||||
def _requestLastSeen(self, buddy):
|
||||
def onSuccess(buddy, lastseen):
|
||||
timestamp = time.localtime(time.localtime()-lastseen)
|
||||
timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp)
|
||||
self.sendMessageToXMPP(buddy, "%s (%s) %s" % (timestring, ago(lastseen), str(lastseen)))
|
||||
|
||||
def onError(errorIqEntity, originalIqEntity):
|
||||
self.sendMessageToXMPP(errorIqEntity.getFrom(), "LastSeen Error")
|
||||
|
||||
self.requestLastSeen(buddy, onSuccess, onError)
|
||||
|
||||
def _sendLocation(self, buddy, message, ID):
|
||||
latitude,longitude = message.split(':')[1].split(',')
|
||||
waId = self.sendLocation(buddy, float(latitude), float(longitude))
|
||||
self.msgIDs[waId] = MsgIDs(ID, waId)
|
||||
self.logger.info("WA Location Message send to %s with ID %s", buddy, waId)
|
||||
|
||||
def sendMessageToXMPP(self, buddy, messageContent, timestamp = "", nickname = ""):
|
||||
if timestamp:
|
||||
timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp))
|
||||
|
||||
if self.initialized == False:
|
||||
self.logger.debug("Message queued from %s to %s: %s" %
|
||||
(buddy, self.legacyName, messageContent))
|
||||
self.offlineQueue.append((buddy, messageContent, timestamp))
|
||||
else:
|
||||
self.logger.debug("Message sent from %s to %s: %s" % (
|
||||
buddy, self.legacyName, messageContent))
|
||||
self.backend.handleMessage(self.user, buddy, messageContent, "",
|
||||
"", timestamp)
|
||||
|
||||
def sendGroupMessageToXMPP(self, room, number, messageContent, timestamp = "", defaultname = ""):
|
||||
if timestamp:
|
||||
timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp))
|
||||
|
||||
if self.initialized == False:
|
||||
self.logger.debug("Group message queued from %s to %s: %s" %
|
||||
(number, room, messageContent))
|
||||
|
||||
if room not in self.groupOfflineQueue:
|
||||
self.groupOfflineQueue[room] = [ ]
|
||||
|
||||
self.groupOfflineQueue[room].append(
|
||||
(number, messageContent, timestamp)
|
||||
)
|
||||
else:
|
||||
self.logger.debug("Group message sent from %s to %s: %s" %
|
||||
(number, room, messageContent))
|
||||
try:
|
||||
group = self.groups[room]
|
||||
# Update nickname
|
||||
try:
|
||||
if defaultname != "" and group.participants[number] == number:
|
||||
group.changeNick(number, defaultname)
|
||||
if self.buddies[number].nick != "":
|
||||
group.changeNick(number, self.buddies[number].nick)
|
||||
except KeyError:
|
||||
pass
|
||||
nick = group.participants[number]
|
||||
if group.joined:
|
||||
self.backend.handleMessage(self.user, room, messageContent,
|
||||
nick, "", timestamp)
|
||||
else:
|
||||
self.bot.send("You have received a message in group: %s@%s"
|
||||
% (room, self.backend.spectrum_jid))
|
||||
self.bot.send("Join the group in order to reply")
|
||||
self.bot.send("%s: %s" % (nick, messageContent))
|
||||
except KeyError:
|
||||
self.logger.warn("Group is not in group list")
|
||||
self.backend.handleMessage(self.user, self._shortenGroupId(room),
|
||||
messageContent, number, "", timestamp)
|
||||
|
||||
|
||||
def changeStatus(self, status):
|
||||
if status != self.status:
|
||||
self.logger.info("Status changed: %s" % status)
|
||||
self.status = status
|
||||
|
||||
if status == Spectrum2.protocol_pb2.STATUS_ONLINE \
|
||||
or status == Spectrum2.protocol_pb2.STATUS_FFC:
|
||||
self.sendPresence(True)
|
||||
else:
|
||||
self.sendPresence(False)
|
||||
|
||||
def changeStatusMessage(self, statusMessage):
|
||||
if (statusMessage != self.statusMessage) or (self.initialized == False):
|
||||
self.statusMessage = statusMessage
|
||||
self.setStatus(statusMessage)
|
||||
self.logger.info("Status message changed: %s" % statusMessage)
|
||||
|
||||
#if self.initialized == False:
|
||||
# self.sendOfflineMessages()
|
||||
# self.bot.call("welcome")
|
||||
# self.initialized = True
|
||||
|
||||
def sendOfflineMessages(self):
|
||||
# Flush Queues
|
||||
while self.offlineQueue:
|
||||
msg = self.offlineQueue.pop(0)
|
||||
self.backend.handleMessage(self.user, msg[0], msg[1], "", "", msg[2])
|
||||
|
||||
# Called when user logs in to initialize the roster
|
||||
def loadBuddies(self, buddies):
|
||||
self.buddies.load(buddies)
|
||||
|
||||
# also for adding a new buddy
|
||||
def updateBuddy(self, buddy, nick, groups, image_hash = None):
|
||||
if buddy != "bot":
|
||||
self.buddies.update(buddy, nick, groups, image_hash)
|
||||
|
||||
def removeBuddy(self, buddy):
|
||||
if buddy != "bot":
|
||||
self.logger.info("Buddy removed: %s" % buddy)
|
||||
self.buddies.remove(buddy)
|
||||
|
||||
def requestVCard(self, buddy, ID=None):
|
||||
self.buddies.requestVCard(buddy, ID)
|
||||
|
||||
def createThumb(self, size=100, raw=False):
|
||||
img = Image.open(self.imgPath)
|
||||
width, height = img.size
|
||||
img_thumbnail = self.imgPath + '_thumbnail'
|
||||
|
||||
if width > height:
|
||||
nheight = float(height) / width * size
|
||||
nwidth = size
|
||||
else:
|
||||
nwidth = float(width) / height * size
|
||||
nheight = size
|
||||
|
||||
img.thumbnail((nwidth, nheight), Image.ANTIALIAS)
|
||||
img.save(img_thumbnail, 'JPEG')
|
||||
|
||||
with open(img_thumbnail, 'rb') as imageFile:
|
||||
raw = base64.b64encode(imageFile.read())
|
||||
|
||||
return raw
|
||||
|
||||
# Not used
|
||||
def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast):
|
||||
buddy = jid.split("@")[0]
|
||||
self.logger.info("Location received from %s: %s, %s" % (buddy, latitude, longitude))
|
||||
|
||||
url = "http://maps.google.de?%s" % urllib.urlencode({ "q": "%s %s" % (latitude, longitude) })
|
||||
self.sendMessageToXMPP(buddy, utils.shorten(url))
|
||||
if receiptRequested:
|
||||
self.call("message_ack", (jid, messageId))
|
||||
|
||||
def onGroupSubjectReceived(self, messageId, gjid, jid, subject, timestamp, receiptRequested):
|
||||
room = gjid.split("@")[0]
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
self.backend.handleSubject(self.user, room, subject, buddy)
|
||||
if receiptRequested:
|
||||
self.call("subject_ack", (gjid, messageId))
|
||||
|
||||
# Yowsup Notifications
|
||||
def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested):
|
||||
room = gjid.split("@")[0]
|
||||
buddy = jid.split("@")[0]
|
||||
|
||||
self.logger.info("Removed %s from room %s" % (buddy, room))
|
||||
|
||||
self.backend.handleParticipantChanged(self.user, buddy, room, Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE, Spectrum2.protocol_pb2.STATUS_NONE) # TODO
|
||||
|
||||
if receiptRequested: self.call("notification_ack", (gjid, messageId))
|
||||
|
||||
def onContactProfilePictureUpdated(self, jid, timestamp, messageId, pictureId, receiptRequested):
|
||||
# TODO
|
||||
if receiptRequested:
|
||||
self.call("notification_ack", (jid, messageId))
|
||||
|
||||
def onGroupPictureUpdated(self, jid, author, timestamp, messageId, pictureId, receiptRequested):
|
||||
# TODO
|
||||
if receiptRequested:
|
||||
self.call("notification_ack", (jid, messageId))
|
|
@ -0,0 +1,20 @@
|
|||
import queue
|
||||
import threading
|
||||
|
||||
# This queue is for other threads that want to execute code in the main thread
|
||||
eventQueue = queue.Queue()
|
||||
|
||||
def runInThread(threadFunc, callback):
|
||||
"""
|
||||
Executes threadFunc in a new thread. The result of threadFunc will be
|
||||
pass as the first argument to callback. callback will be called in the main
|
||||
thread.
|
||||
"""
|
||||
def helper():
|
||||
# Execute threadfunc in new thread
|
||||
result = threadFunc()
|
||||
# Queue callback to be call in main thread
|
||||
eventQueue.put(lambda: callback(result))
|
||||
|
||||
thread = threading.Thread(target=helper)
|
||||
thread.start()
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
import argparse
|
||||
import traceback
|
||||
import logging
|
||||
import asyncore
|
||||
import sys
|
||||
import queue
|
||||
|
||||
import Spectrum2
|
||||
from yowsup.common import YowConstants
|
||||
from yowsup.stacks import YowStack
|
||||
|
||||
from .whatsappbackend import WhatsAppBackend
|
||||
from . import threadutils
|
||||
|
||||
# Arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--debug', action='store_true')
|
||||
parser.add_argument('--log', type=str)
|
||||
parser.add_argument('--host', type=str, required=True)
|
||||
parser.add_argument('--port', type=int, required=True)
|
||||
parser.add_argument('--service.backend_id', metavar="ID", type=int, required=True)
|
||||
parser.add_argument('-j', type=str, metavar="JID", required=True)
|
||||
parser.add_argument('config', type=str)
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
YowConstants.PATH_STORAGE='/var/lib/spectrum2/' + args.j
|
||||
|
||||
if args.log is None:
|
||||
args.log = '/var/log/spectrum2/' + args.j + '/backends/backend.log'
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(
|
||||
filename = args.log,
|
||||
format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s",
|
||||
level = logging.DEBUG if args.debug else logging.INFO
|
||||
)
|
||||
|
||||
if args.config is not None:
|
||||
specConf = Spectrum2.Config(args.config)
|
||||
else:
|
||||
specConf = None
|
||||
|
||||
# Handler
|
||||
def handleTransportData(data):
|
||||
try:
|
||||
plugin.handleDataRead(data)
|
||||
except SystemExit as e:
|
||||
raise e
|
||||
except:
|
||||
logger = logging.getLogger('transwhat')
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
closed = False
|
||||
def connectionClosed():
|
||||
global closed
|
||||
closed = True
|
||||
|
||||
# Main
|
||||
io = Spectrum2.IOChannel(args.host, args.port, handleTransportData, connectionClosed)
|
||||
|
||||
plugin = WhatsAppBackend(io, args.j, specConf)
|
||||
|
||||
plugin.handleBackendConfig({
|
||||
'features': [
|
||||
('send_buddies_on_login', 1),
|
||||
('muc', 'true'),
|
||||
],
|
||||
})
|
||||
|
||||
def main():
|
||||
while True:
|
||||
try:
|
||||
asyncore.loop(timeout=1.0, count=10, use_poll = True)
|
||||
try:
|
||||
callback = YowStack._YowStack__detachedQueue.get(False) # doesn't block
|
||||
callback()
|
||||
except queue.Empty:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
if closed:
|
||||
break
|
||||
|
||||
while True:
|
||||
try:
|
||||
callback = threadutils.eventQueue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
else:
|
||||
callback()
|
||||
except SystemExit:
|
||||
break
|
||||
except:
|
||||
logger = logging.getLogger('transwhat')
|
||||
logger.error(traceback.format_exc())
|
|
@ -0,0 +1,144 @@
|
|||
import logging
|
||||
import Spectrum2
|
||||
|
||||
from .session import Session
|
||||
from .registersession import RegisterSession
|
||||
|
||||
class WhatsAppBackend(Spectrum2.Backend):
|
||||
def __init__(self, io, spectrum_jid, specConf):
|
||||
Spectrum2.Backend.__init__(self)
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.io = io
|
||||
self.specConf = specConf
|
||||
self.sessions = { }
|
||||
self.spectrum_jid = spectrum_jid
|
||||
# Used to prevent duplicate messages
|
||||
self.lastMsgId = {}
|
||||
|
||||
self.logger.debug("Backend started")
|
||||
|
||||
# RequestsHandlers
|
||||
def handleLoginRequest(self, user, legacyName, password, extra):
|
||||
self.logger.debug("handleLoginRequest(user=%s, legacyName=%s)" % (user, legacyName))
|
||||
# Key word means we should register a new password
|
||||
if password == 'register':
|
||||
if user not in self.sessions:
|
||||
self.sessions[user] = RegisterSession(self, user, legacyName, extra)
|
||||
else:
|
||||
if user not in self.sessions:
|
||||
self.sessions[user] = Session(self, user, legacyName, extra)
|
||||
|
||||
self.sessions[user].login(password)
|
||||
|
||||
def handleLogoutRequest(self, user, legacyName):
|
||||
self.logger.debug("handleLogoutRequest(user=%s, legacyName=%s)" % (user, legacyName))
|
||||
if user in self.sessions:
|
||||
self.sessions[user].logout()
|
||||
del self.sessions[user]
|
||||
|
||||
def handleMessageSendRequest(self, user, buddy, message, xhtml="", ID=""):
|
||||
self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s, xhtml=%s, ID=%s)" %
|
||||
( user, buddy, message, xhtml, ID))
|
||||
# For some reason spectrum occasionally sends to identical messages to
|
||||
# a buddy, one to the bare jid and one to the /bot resource. This
|
||||
# causes duplicate messages. Thus we should not send consecutive
|
||||
# messages with the same id
|
||||
if ID == '':
|
||||
self.sessions[user].sendMessageToWA(buddy, message, ID, xhtml)
|
||||
elif user not in self.lastMsgId or self.lastMsgId[user] != ID:
|
||||
self.sessions[user].sendMessageToWA(buddy, message, ID, xhtml)
|
||||
self.lastMsgId[user] = ID
|
||||
|
||||
def handleJoinRoomRequest(self, user, room, nickname, pasword):
|
||||
self.logger.debug("handleJoinRoomRequest(user=%s, room=%s, nickname=%s)" % (user, room, nickname))
|
||||
self.sessions[user].joinRoom(room, nickname)
|
||||
|
||||
def handleLeaveRoomRequest(self, user, room):
|
||||
self.logger.debug("handleLeaveRoomRequest(user=%s, room=%s)" % (user, room))
|
||||
self.sessions[user].leaveRoom(room)
|
||||
|
||||
def handleStatusChangeRequest(self, user, status, statusMessage):
|
||||
self.logger.debug("handleStatusChangeRequest(user=%s, status=%d, statusMessage=%s)" % (user, status, statusMessage))
|
||||
self.sessions[user].changeStatusMessage(statusMessage)
|
||||
self.sessions[user].changeStatus(status)
|
||||
|
||||
def handleBuddies(self, buddies):
|
||||
"""Called when user logs in. Used to initialize roster."""
|
||||
self.logger.debug("handleBuddies(buddies=%s)" % buddies)
|
||||
buddies = [b for b in buddies.buddy]
|
||||
if len(buddies) > 0:
|
||||
user = buddies[0].userName
|
||||
self.sessions[user].loadBuddies(buddies)
|
||||
|
||||
def handleBuddyUpdatedRequest(self, user, buddy, nick, groups):
|
||||
self.logger.debug("handleBuddyUpdatedRequest(user=%s, buddy=%s, nick=%s, groups=%s)" % (user, buddy, nick, groups))
|
||||
self.sessions[user].updateBuddy(buddy, nick, groups)
|
||||
|
||||
def handleBuddyRemovedRequest(self, user, buddy, groups):
|
||||
self.logger.debug("handleBuddyRemovedRequest(user=%s, buddy=%s, groups=%s)" % (user, buddy, groups))
|
||||
self.sessions[user].removeBuddy(buddy)
|
||||
|
||||
def handleTypingRequest(self, user, buddy):
|
||||
self.logger.debug("handleTypingRequest(user=%s, buddy=%s)" % (user, buddy))
|
||||
self.sessions[user].sendTypingStarted(buddy)
|
||||
|
||||
def handleTypedRequest(self, user, buddy):
|
||||
self.logger.debug("handleTypedRequest(user=%s, buddy=%s)" % (user, buddy))
|
||||
self.sessions[user].sendTypingStopped(buddy)
|
||||
|
||||
def handleStoppedTypingRequest(self, user, buddy):
|
||||
self.logger.debug("handleStoppedTypingRequest(user=%s, buddy=%s)" % (user, buddy))
|
||||
self.sessions[user].sendTypingStopped(buddy)
|
||||
|
||||
def handleVCardRequest(self, user, buddy, ID):
|
||||
self.logger.debug("handleVCardRequest(user=%s, buddy=%s, ID=%s)" % (user, buddy, ID))
|
||||
self.sessions[user].requestVCard(buddy, ID)
|
||||
|
||||
def handleVCardUpdatedRequest(self, user, photo, nickname):
|
||||
self.logger.debug("handleVCardUpdatedRequest(user=%s, nickname=%s)" % (user, nickname))
|
||||
self.sessions[user].setProfilePicture(photo)
|
||||
|
||||
def handleBuddyBlockToggled(self, user, buddy, blocked):
|
||||
self.logger.debug("handleBuddyBlockedToggled(user=%s, buddy=%s, blocked=%s)" % (user, buddy, blocked))
|
||||
|
||||
def relogin(self, user, legacyName, password, extra):
|
||||
"""
|
||||
Used to re-initialize the session object. Used when finished with
|
||||
registration session and the user needs to login properly
|
||||
"""
|
||||
self.logger.debug("relogin(user=%s, legacyName=%s)" % (user, legacyName))
|
||||
# Change password in spectrum database
|
||||
self.handleQuery('register %s %s %s' % (user, legacyName, password))
|
||||
# Key word means we should register a new password
|
||||
if password == 'register': # This shouldn't happen, but just in case
|
||||
self.sessions[user] = RegisterSession(self, user, legacyName, extra)
|
||||
else:
|
||||
self.sessions[user] = Session(self, user, legacyName, extra)
|
||||
self.sessions[user].login(password)
|
||||
|
||||
# TODO
|
||||
def handleAttentionRequest(self, user, buddy, message):
|
||||
pass
|
||||
|
||||
def handleFTStartRequest(self, user, buddy, fileName, size, ftID):
|
||||
self.logger.debug('File send request %s, for user %s, from %s, size: %s' %
|
||||
(fileName, user, buddy, size))
|
||||
|
||||
def handleFTFinishRequest(self, user, buddy, fileName, size, ftID):
|
||||
pass
|
||||
|
||||
def handleFTPauseRequest(self, ftID):
|
||||
pass
|
||||
|
||||
def handleFTContinueRequest(self, ftID):
|
||||
pass
|
||||
|
||||
def handleRawXmlRequest(self, xml):
|
||||
pass
|
||||
|
||||
def handleMessageAckRequest(self, user, legacyName, ID = 0):
|
||||
self.logger.info("Meassage ACK request for %s !!" % legacyName)
|
||||
|
||||
def sendData(self, data):
|
||||
self.io.sendData(data)
|
||||
|
|
@ -0,0 +1,856 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from yowsup import env
|
||||
from yowsup.env import YowsupEnv
|
||||
from yowsup.stacks import YowStack, YowStackBuilder
|
||||
from yowsup.common import YowConstants
|
||||
from yowsup.layers import YowLayerEvent, YowParallelLayer
|
||||
|
||||
# Layers
|
||||
from yowsup.layers.axolotl import AxolotlSendLayer, AxolotlControlLayer, AxolotlReceivelayer
|
||||
from yowsup.layers.auth import YowAuthenticationProtocolLayer
|
||||
from yowsup.layers.coder import YowCoderLayer
|
||||
from yowsup.layers.logger import YowLoggerLayer
|
||||
from yowsup.layers.network import YowNetworkLayer
|
||||
from yowsup.layers.protocol_messages import YowMessagesProtocolLayer
|
||||
from yowsup.layers.protocol_media import YowMediaProtocolLayer
|
||||
from yowsup.layers.protocol_acks import YowAckProtocolLayer
|
||||
from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer
|
||||
from yowsup.layers.protocol_groups import YowGroupsProtocolLayer
|
||||
from yowsup.layers.protocol_presence import YowPresenceProtocolLayer
|
||||
from yowsup.layers.protocol_ib import YowIbProtocolLayer
|
||||
from yowsup.layers.protocol_notifications import YowNotificationsProtocolLayer
|
||||
from yowsup.layers.protocol_iq import YowIqProtocolLayer
|
||||
from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer
|
||||
from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer
|
||||
from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer
|
||||
from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer
|
||||
from yowsup.layers.protocol_calls import YowCallsProtocolLayer
|
||||
from yowsup.layers.axolotl.props import PROP_IDENTITY_AUTOTRUST
|
||||
|
||||
# ProtocolEntities
|
||||
from yowsup.layers.protocol_acks.protocolentities import *
|
||||
from yowsup.layers.protocol_chatstate.protocolentities import *
|
||||
from yowsup.layers.protocol_contacts.protocolentities import *
|
||||
from yowsup.layers.protocol_groups.protocolentities import *
|
||||
from yowsup.layers.protocol_media.protocolentities import *
|
||||
from yowsup.layers.protocol_notifications.protocolentities import *
|
||||
from yowsup.layers.protocol_messages.protocolentities import *
|
||||
from yowsup.layers.protocol_presence.protocolentities import *
|
||||
from yowsup.layers.protocol_profiles.protocolentities import *
|
||||
from yowsup.layers.protocol_privacy.protocolentities import *
|
||||
from yowsup.layers.protocol_receipts.protocolentities import *
|
||||
from yowsup.layers.protocol_iq.protocolentities import *
|
||||
from yowsup.layers.protocol_media.mediauploader import MediaUploader
|
||||
from yowsup.layers.protocol_media.mediadownloader import MediaDownloader
|
||||
|
||||
# Registration
|
||||
from yowsup.registration import WACodeRequest
|
||||
from yowsup.registration import WARegRequest
|
||||
|
||||
from functools import partial
|
||||
|
||||
class YowsupApp(object):
|
||||
def __init__(self):
|
||||
env.CURRENT_ENV = env.AndroidYowsupEnv()
|
||||
|
||||
layers = (YowsupAppLayer,
|
||||
YowParallelLayer((YowAuthenticationProtocolLayer,
|
||||
YowMessagesProtocolLayer,
|
||||
YowReceiptProtocolLayer,
|
||||
YowAckProtocolLayer,
|
||||
YowMediaProtocolLayer,
|
||||
YowIbProtocolLayer,
|
||||
YowIqProtocolLayer,
|
||||
YowNotificationsProtocolLayer,
|
||||
YowContactsIqProtocolLayer,
|
||||
YowChatstateProtocolLayer,
|
||||
YowCallsProtocolLayer,
|
||||
YowPrivacyProtocolLayer,
|
||||
YowProfilesProtocolLayer,
|
||||
YowGroupsProtocolLayer,
|
||||
YowPresenceProtocolLayer)),
|
||||
AxolotlControlLayer,
|
||||
YowParallelLayer((AxolotlSendLayer, AxolotlReceivelayer)),
|
||||
YowCoderLayer,
|
||||
YowNetworkLayer
|
||||
)
|
||||
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
stackBuilder = YowStackBuilder()
|
||||
|
||||
self.stack = stackBuilder \
|
||||
.pushDefaultLayers() \
|
||||
.push(YowsupAppLayer) \
|
||||
.build()
|
||||
self.stack.broadcastEvent(
|
||||
YowLayerEvent(YowsupAppLayer.EVENT_START, caller = self)
|
||||
)
|
||||
|
||||
def login(self, username, password):
|
||||
"""Login to yowsup
|
||||
|
||||
Should result in onAuthSuccess or onAuthFailure to be called.
|
||||
|
||||
Args:
|
||||
- username: (str) username in the form of 1239482382 (country code
|
||||
and cellphone number)
|
||||
|
||||
- password: (str) base64 encoded password
|
||||
"""
|
||||
self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS,
|
||||
(username, password))
|
||||
self.stack.setProp(PROP_IDENTITY_AUTOTRUST, True)
|
||||
# self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5)
|
||||
|
||||
try:
|
||||
self.stack.broadcastEvent(
|
||||
YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT))
|
||||
except TypeError as e: # Occurs when password is not correctly formated
|
||||
self.onAuthFailure('password not base64 encoded')
|
||||
# try:
|
||||
# self.stack.loop(timeout=0.5, discrete=0.5)
|
||||
# except AuthError as e: # For some reason Yowsup throws an exception
|
||||
# self.onAuthFailure("%s" % e)
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
Logout from whatsapp
|
||||
"""
|
||||
self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT))
|
||||
|
||||
def sendReceipt(self, _id, _from, read, participant):
|
||||
"""
|
||||
Send a receipt (delivered: double-tick, read: blue-ticks)
|
||||
|
||||
Args:
|
||||
- _id: id of message received
|
||||
- _from: jid of person who sent the message
|
||||
- read: ('read' or None) None is just delivered, 'read' is read
|
||||
- participant
|
||||
"""
|
||||
self.logger.debug(u'Sending receipt to whatsapp: %s', [_id, _from, read, participant])
|
||||
receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant)
|
||||
self.sendEntity(receipt)
|
||||
|
||||
def downloadMedia(self, url, onSuccess = None, onFailure = None):
|
||||
downloader = MediaDownloader(onSuccess, onFailure)
|
||||
downloader.download(url)
|
||||
|
||||
def sendTextMessage(self, to, message):
|
||||
"""
|
||||
Sends a text message
|
||||
|
||||
Args:
|
||||
- to: (xxxxxxxxxx@s.whatsapp.net) who to send the message to
|
||||
- message: (str) the body of the message
|
||||
"""
|
||||
messageEntity = TextMessageProtocolEntity(message.encode('utf-8'), to = to)
|
||||
self.sendEntity(messageEntity)
|
||||
return messageEntity.getId()
|
||||
|
||||
def sendLocation(self, to, latitude, longitude):
|
||||
messageEntity = LocationMediaMessageProtocolEntity(latitude,longitude, None, None, "raw", to = to)
|
||||
self.sendEntity(messageEntity)
|
||||
|
||||
return messageEntity.getId()
|
||||
|
||||
def sendImage(self, jid, path, caption = None, onSuccess = None, onFailure = None):
|
||||
entity = RequestUploadIqProtocolEntity(RequestUploadIqProtocolEntity.MEDIA_TYPE_IMAGE, filePath=path)
|
||||
successFn = lambda successEntity, originalEntity: self.onRequestUploadResult(jid, path, successEntity, originalEntity, caption, onSuccess, onFailure)
|
||||
errorFn = lambda errorEntity, originalEntity: self.onRequestUploadError(jid, path, errorEntity, originalEntity)
|
||||
|
||||
self.sendIq(entity, successFn, errorFn)
|
||||
|
||||
def onRequestUploadResult(self, jid, filePath, resultRequestUploadIqProtocolEntity, requestUploadIqProtocolEntity, caption = None, onSuccess=None, onFailure=None):
|
||||
if requestUploadIqProtocolEntity.mediaType == RequestUploadIqProtocolEntity.MEDIA_TYPE_AUDIO:
|
||||
doSendFn = self.doSendAudio
|
||||
else:
|
||||
doSendFn = self.doSendImage
|
||||
|
||||
if resultRequestUploadIqProtocolEntity.isDuplicate():
|
||||
doSendFn(filePath, resultRequestUploadIqProtocolEntity.getUrl(), jid,
|
||||
resultRequestUploadIqProtocolEntity.getIp(), caption, onSuccess, onFailure)
|
||||
else:
|
||||
successFn = lambda filePath, jid, url: doSendFn(filePath, url.encode('ascii','ignore'), jid, resultRequestUploadIqProtocolEntity.getIp(), caption, onSuccess, onFailure)
|
||||
ownNumber = self.stack.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(full=False)
|
||||
mediaUploader = MediaUploader(jid, ownNumber, filePath,
|
||||
resultRequestUploadIqProtocolEntity.getUrl(),
|
||||
resultRequestUploadIqProtocolEntity.getResumeOffset(),
|
||||
successFn, self.onUploadError, self.onUploadProgress, asynchronous=False)
|
||||
mediaUploader.start()
|
||||
|
||||
def onRequestUploadError(self, jid, path, errorRequestUploadIqProtocolEntity, requestUploadIqProtocolEntity):
|
||||
self.logger.error("Request upload for file %s for %s failed" % (path, jid))
|
||||
|
||||
def onUploadError(self, filePath, jid, url):
|
||||
self.logger.error("Upload file %s to %s for %s failed!" % (filePath, url, jid))
|
||||
|
||||
def onUploadProgress(self, filePath, jid, url, progress):
|
||||
self.logger.info("%s => %s, %d%% \r" % (os.path.basename(filePath), jid, progress))
|
||||
pass
|
||||
|
||||
def doSendImage(self, filePath, url, to, ip = None, caption = None, onSuccess = None, onFailure = None):
|
||||
entity = ImageDownloadableMediaMessageProtocolEntity.fromFilePath(filePath, url, ip, to, caption = caption)
|
||||
self.sendEntity(entity)
|
||||
#self.msgIDs[entity.getId()] = MsgIDs(self.imgMsgId, entity.getId())
|
||||
if onSuccess is not None:
|
||||
onSuccess(entity.getId())
|
||||
return entity.getId()
|
||||
|
||||
def doSendAudio(self, filePath, url, to, ip = None, caption = None, onSuccess = None, onFailure = None):
|
||||
entity = AudioDownloadableMediaMessageProtocolEntity.fromFilePath(filePath, url, ip, to)
|
||||
self.sendEntity(entity)
|
||||
#self.msgIDs[entity.getId()] = MsgIDs(self.imgMsgId, entity.getId())
|
||||
if onSuccess is not None:
|
||||
onSuccess(entity.getId())
|
||||
return entity.getId()
|
||||
|
||||
def sendPresence(self, available):
|
||||
"""
|
||||
Send presence to whatsapp
|
||||
|
||||
Args:
|
||||
- available: (boolean) True if available false otherwise
|
||||
"""
|
||||
if available:
|
||||
self.sendEntity(AvailablePresenceProtocolEntity())
|
||||
else:
|
||||
self.sendEntity(UnavailablePresenceProtocolEntity())
|
||||
|
||||
def subscribePresence(self, phone_number):
|
||||
"""
|
||||
Subscribe to presence updates from phone_number
|
||||
|
||||
Args:
|
||||
- phone_number: (str) The cellphone number of the person to
|
||||
subscribe to
|
||||
"""
|
||||
self.logger.debug("Subscribing to Presence updates from %s" % phone_number)
|
||||
jid = phone_number + '@s.whatsapp.net'
|
||||
entity = SubscribePresenceProtocolEntity(jid)
|
||||
self.sendEntity(entity)
|
||||
|
||||
def unsubscribePresence(self, phone_number):
|
||||
"""
|
||||
Unsubscribe to presence updates from phone_number
|
||||
|
||||
Args:
|
||||
- phone_number: (str) The cellphone number of the person to
|
||||
unsubscribe from
|
||||
"""
|
||||
jid = phone_number + '@s.whatsapp.net'
|
||||
entity = UnsubscribePresenceProtocolEntity(jid)
|
||||
self.sendEntity(entity)
|
||||
|
||||
def leaveGroup(self, group):
|
||||
"""
|
||||
Permanently leave a WhatsApp group
|
||||
|
||||
Args:
|
||||
- group: (str) the group id (e.g. 27831788123-144024456)
|
||||
"""
|
||||
entity = LeaveGroupsIqProtocolEntity([group + '@g.us'])
|
||||
self.sendEntity(entity)
|
||||
|
||||
def setStatus(self, statusText):
|
||||
"""
|
||||
Send status to whatsapp
|
||||
|
||||
Args:
|
||||
- statusTest: (str) Your whatsapp status
|
||||
"""
|
||||
iq = SetStatusIqProtocolEntity(statusText)
|
||||
self.sendIq(iq)
|
||||
|
||||
def setProfilePicture(self, previewPicture, fullPicture = None):
|
||||
"""
|
||||
Requests profile picture of whatsapp user
|
||||
Args:
|
||||
- previewPicture: (bytes) The preview picture
|
||||
- fullPicture: (bytes) The full profile picture
|
||||
"""
|
||||
if fullPicture == None:
|
||||
fullPicture = previewPicture
|
||||
ownJid = self.stack.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(full = True)
|
||||
iq = SetPictureIqProtocolEntity(ownJid, previewPicture, fullPicture)
|
||||
self.sendIq(iq)
|
||||
|
||||
def sendTyping(self, phoneNumber, typing):
|
||||
"""
|
||||
Notify buddy using phoneNumber that you are typing to him
|
||||
|
||||
Args:
|
||||
- phoneNumber: (str) cellphone number of the buddy you are typing to.
|
||||
- typing: (bool) True if you are typing, False if you are not
|
||||
"""
|
||||
jid = phoneNumber + '@s.whatsapp.net'
|
||||
if typing:
|
||||
state = OutgoingChatstateProtocolEntity(
|
||||
ChatstateProtocolEntity.STATE_TYPING, jid
|
||||
)
|
||||
else:
|
||||
state = OutgoingChatstateProtocolEntity(
|
||||
ChatstateProtocolEntity.STATE_PAUSED, jid
|
||||
)
|
||||
self.sendEntity(state)
|
||||
|
||||
def sendSync(self, contacts, delta = False, interactive = True, success = None, failure = None):
|
||||
"""
|
||||
You need to sync new contacts before you interact with
|
||||
them, failure to do so could result in a temporary ban.
|
||||
|
||||
Args:
|
||||
- contacts: ([str]) a list of phone numbers of the
|
||||
contacts you wish to sync
|
||||
- delta: (bool; default: False) If true only send new
|
||||
contacts to sync, if false you should send your full
|
||||
contact list.
|
||||
- interactive: (bool; default: True) Set to false if you are
|
||||
sure this is the first time registering
|
||||
- success: (func) - Callback; Takes three arguments: existing numbers,
|
||||
non-existing numbers, invalid numbers.
|
||||
"""
|
||||
mode = GetSyncIqProtocolEntity.MODE_DELTA if delta else GetSyncIqProtocolEntity.MODE_FULL
|
||||
context = GetSyncIqProtocolEntity.CONTEXT_INTERACTIVE if interactive else GetSyncIqProtocolEntity.CONTEXT_REGISTRATION
|
||||
# International contacts must be preceded by a plus. Other numbers are
|
||||
# considered local.
|
||||
contacts = ['+' + c for c in contacts]
|
||||
iq = GetSyncIqProtocolEntity(contacts, mode, context)
|
||||
def onSuccess(response, request):
|
||||
# Remove leading plus
|
||||
if success is not None:
|
||||
existing = [s[1:] for s in response.inNumbers.keys()]
|
||||
nonexisting = [s[1:] for s in response.outNumbers.keys()]
|
||||
invalid = [s[1:] for s in response.invalidNumbers]
|
||||
success(existing, nonexisting, invalid)
|
||||
|
||||
self.sendIq(iq, onSuccess = onSuccess, onError = failure)
|
||||
|
||||
def requestClientConfig(self, success = None, failure = None):
|
||||
"""I'm not sure what this does, but it might be required on first login."""
|
||||
iq = PushIqProtocolEntity()
|
||||
self.sendIq(iq, onSuccess = success, onError = failure)
|
||||
|
||||
|
||||
def requestPrivacyList(self, success = None, failure = None):
|
||||
"""I'm not sure what this does, but it might be required on first login."""
|
||||
iq = PrivacyListIqProtocolEntity()
|
||||
self.sendIq(iq, onSuccess = success, onError = failure)
|
||||
|
||||
def requestServerProperties(self, success = None, failure = None):
|
||||
"""I'm not sure what this does, but it might be required on first login."""
|
||||
iq = PropsIqProtocolEntity()
|
||||
self.sendIq(iq, onSuccess = success, onError = failure)
|
||||
|
||||
def requestStatuses(self, contacts, success = None, failure = None):
|
||||
"""
|
||||
Request the statuses of a number of users.
|
||||
|
||||
Args:
|
||||
- contacts: ([str]) the phone numbers of users whose statuses you
|
||||
wish to request
|
||||
- success: (func) called when request is successful
|
||||
- failure: (func) called when request has failed
|
||||
"""
|
||||
iq = GetStatusesIqProtocolEntity([c + '@s.whatsapp.net' for c in contacts])
|
||||
def onSuccess(response, request):
|
||||
if success is not None:
|
||||
self.logger.debug("Received Statuses %s" % response)
|
||||
s = {}
|
||||
for k, v in response.statuses.iteritems():
|
||||
s[k.split('@')[0]] = v
|
||||
success(s)
|
||||
|
||||
self.sendIq(iq, onSuccess = onSuccess, onError = failure)
|
||||
|
||||
|
||||
def requestLastSeen(self, phoneNumber, success = None, failure = None):
|
||||
"""
|
||||
Requests when user was last seen.
|
||||
Args:
|
||||
- phone_number: (str) the phone number of the user
|
||||
- success: (func) called when request is successfully processed.
|
||||
The first argument is the number, second argument is the seconds
|
||||
since last seen.
|
||||
- failure: (func) called when request has failed
|
||||
"""
|
||||
iq = LastseenIqProtocolEntity(phoneNumber + '@s.whatsapp.net')
|
||||
self.sendIq(iq, onSuccess = partial(self._lastSeenSuccess, success),
|
||||
onError = failure)
|
||||
|
||||
def _lastSeenSuccess(self, success, response, request):
|
||||
success(response._from.split('@')[0], response.seconds)
|
||||
|
||||
def requestProfilePicture(self, phoneNumber, onSuccess = None, onFailure = None):
|
||||
"""
|
||||
Requests profile picture of whatsapp user
|
||||
Args:
|
||||
- phoneNumber: (str) the phone number of the user
|
||||
- onSuccess: (func) called when request is successfully processed.
|
||||
- onFailure: (func) called when request has failed
|
||||
"""
|
||||
iq = GetPictureIqProtocolEntity(phoneNumber + '@s.whatsapp.net')
|
||||
self.sendIq(iq, onSuccess = onSuccess, onError = onFailure)
|
||||
|
||||
def requestGroupsList(self, onSuccess = None, onFailure = None):
|
||||
iq = ListGroupsIqProtocolEntity()
|
||||
self.sendIq(iq, onSuccess = onSuccess, onError = onFailure)
|
||||
|
||||
def requestGroupInfo(self, group, onSuccess = None, onFailure = None):
|
||||
"""
|
||||
Request info on a specific group (includes participants, subject, owner etc.)
|
||||
|
||||
Args:
|
||||
- group: (str) the group id in the form of xxxxxxxxx-xxxxxxxx
|
||||
- onSuccess: (func) called when request is successfully processed.
|
||||
- onFailure: (func) called when request is has failed
|
||||
"""
|
||||
iq = InfoGroupsIqProtocolEntity(group + '@g.us')
|
||||
self.sendIq(iq, onSuccess = onSuccess, onError = onFailure)
|
||||
|
||||
def requestSMSCode(self, countryCode, phoneNumber):
|
||||
"""
|
||||
Request an sms regitration code. WARNING: this function is blocking
|
||||
|
||||
Args:
|
||||
countryCode: The country code of the phone you wish to register
|
||||
phoneNumber: phoneNumber of the phone you wish to register without
|
||||
the country code.
|
||||
"""
|
||||
request = WACodeRequest(countryCode, phoneNumber)
|
||||
return request.send()
|
||||
|
||||
def requestPassword(self, countryCode, phoneNumber, smsCode):
|
||||
"""
|
||||
Request a password. WARNING: this function is blocking
|
||||
|
||||
Args:
|
||||
countryCode: The country code of the phone you wish to register
|
||||
phoneNumber: phoneNumber of the phone you wish to register without
|
||||
the country code.
|
||||
smsCode: The sms code that you asked for previously
|
||||
"""
|
||||
smsCode = smsCode.replace('-', '')
|
||||
request = WARegRequest(countryCode, phoneNumber, smsCode)
|
||||
return request.send()
|
||||
|
||||
|
||||
|
||||
def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t):
|
||||
"""
|
||||
Called when login is successful.
|
||||
|
||||
Args:
|
||||
- status
|
||||
- kind
|
||||
- creation
|
||||
- expiration
|
||||
- props
|
||||
- nonce
|
||||
- t
|
||||
"""
|
||||
pass
|
||||
|
||||
def onAuthFailure(self, reason):
|
||||
"""
|
||||
Called when login is a failure
|
||||
|
||||
Args:
|
||||
- reason: (str) Reason for the login failure
|
||||
"""
|
||||
pass
|
||||
|
||||
def onReceipt(self, _id, _from, timestamp, type, participant, offline, items):
|
||||
"""
|
||||
Called when a receipt is received (double tick or blue tick)
|
||||
|
||||
Args
|
||||
- _id
|
||||
- _from
|
||||
- timestamp
|
||||
- type: Is 'read' for blue ticks and None for double-ticks
|
||||
- participant: (dxxxxxxxxxx@s.whatsapp.net) delivered to or
|
||||
read by this participant in group
|
||||
- offline: (True, False or None)
|
||||
- items
|
||||
"""
|
||||
pass
|
||||
|
||||
def onAck(self, _id,_class, _from, timestamp):
|
||||
"""
|
||||
Called when Ack is received
|
||||
|
||||
Args:
|
||||
- _id
|
||||
- _class: ('message', 'receipt' or something else?)
|
||||
- _from
|
||||
- timestamp
|
||||
"""
|
||||
pass
|
||||
|
||||
def onPresenceReceived(self, _type, name, _from, last):
|
||||
"""
|
||||
Called when presence (e.g. available, unavailable) is received
|
||||
from whatsapp
|
||||
|
||||
Args:
|
||||
- _type: (str) 'available' or 'unavailable'
|
||||
- _name
|
||||
- _from
|
||||
- _last
|
||||
"""
|
||||
pass
|
||||
|
||||
def onDisconnect(self):
|
||||
"""
|
||||
Called when disconnected from whatsapp
|
||||
"""
|
||||
|
||||
def onContactTyping(self, number):
|
||||
"""
|
||||
Called when contact starts to type
|
||||
|
||||
Args:
|
||||
- number: (str) cellphone number of contact
|
||||
"""
|
||||
pass
|
||||
|
||||
def onContactPaused(self, number):
|
||||
"""
|
||||
Called when contact stops typing
|
||||
|
||||
Args:
|
||||
- number: (str) cellphone number of contact
|
||||
"""
|
||||
pass
|
||||
|
||||
def onTextMessage(self, _id, _from, to, notify, timestamp, participant, offline, retry, body):
|
||||
"""
|
||||
Called when text message is received
|
||||
|
||||
Args:
|
||||
- _id:
|
||||
- _from: (str) jid of of sender
|
||||
- to:
|
||||
- notify: (str) human readable name of _from (e.g. John Smith)
|
||||
- timestamp:
|
||||
- participant: (str) jid of user who sent the message in a groupchat
|
||||
- offline:
|
||||
- retry:
|
||||
- body: The content of the message
|
||||
"""
|
||||
pass
|
||||
|
||||
def onImage(self, entity):
|
||||
"""
|
||||
Called when image message is received
|
||||
|
||||
Args:
|
||||
- entity: ImageDownloadableMediaMessageProtocolEntity
|
||||
"""
|
||||
pass
|
||||
|
||||
def onAudio(self, entity):
|
||||
"""
|
||||
Called when audio message is received
|
||||
|
||||
Args:
|
||||
- entity: AudioDownloadableMediaMessageProtocolEntity
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def onVideo(self, entity):
|
||||
"""
|
||||
Called when video message is received
|
||||
|
||||
Args:
|
||||
- entity: VideoDownloadableMediaMessageProtocolEntity
|
||||
"""
|
||||
pass
|
||||
|
||||
def onLocation(self, entity):
|
||||
"""
|
||||
Called when location message is received
|
||||
|
||||
Args:
|
||||
- entity: LocationMediaMessageProtocolEntity
|
||||
"""
|
||||
pass
|
||||
|
||||
def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant):
|
||||
"""
|
||||
Called when VCard message is received
|
||||
|
||||
Args:
|
||||
- _id: (str) id of entity
|
||||
- _from:
|
||||
- name:
|
||||
- card_data:
|
||||
- to:
|
||||
- notify:
|
||||
- timestamp:
|
||||
- participant:
|
||||
"""
|
||||
pass
|
||||
|
||||
def onAddedToGroup(self, entity):
|
||||
"""Called when the user has been added to a new group"""
|
||||
pass
|
||||
|
||||
def onParticipantsAddedToGroup(self, entity):
|
||||
"""Called when participants have been added to a group"""
|
||||
pass
|
||||
|
||||
def onParticipantsRemovedFromGroup(self, group, participants):
|
||||
"""Called when participants have been removed from a group
|
||||
|
||||
Args:
|
||||
- group: (str) id of the group (e.g. 27831788123-144024456)
|
||||
- participants: (list) jids of participants that are removed
|
||||
"""
|
||||
pass
|
||||
|
||||
def onSubjectChanged(self, group, subject, subjectOwner, timestamp):
|
||||
"""Called when someone changes the grousp subject
|
||||
|
||||
Args:
|
||||
- group: (str) id of the group (e.g. 27831788123-144024456)
|
||||
- subject: (str) the new subject
|
||||
- subjectOwner: (str) the number of the person who changed the subject
|
||||
- timestamp: (str) time the subject was changed
|
||||
"""
|
||||
pass
|
||||
|
||||
def onContactStatusChanged(self, number, status):
|
||||
"""Called when a contacts changes their status
|
||||
|
||||
Args:
|
||||
number: (str) the number of the contact who changed their status
|
||||
status: (str) the new status
|
||||
"""
|
||||
pass
|
||||
|
||||
def onContactPictureChanged(self, number):
|
||||
"""Called when a contact changes their profile picture
|
||||
Args
|
||||
number: (str) the number of the contact who changed their picture
|
||||
"""
|
||||
pass
|
||||
|
||||
def onContactRemoved(self, number):
|
||||
"""Called when a contact has been removed
|
||||
|
||||
Args:
|
||||
number: (str) the number of the contact who has been removed
|
||||
"""
|
||||
pass
|
||||
|
||||
def onContactAdded(self, number, nick):
|
||||
"""Called when a contact has been added
|
||||
|
||||
Args:
|
||||
number: (str) contacts number
|
||||
nick: (str) contacts nickname
|
||||
"""
|
||||
pass
|
||||
|
||||
def onContactUpdated(self, oldNumber, newNumber):
|
||||
"""Called when a contact has changed their number
|
||||
|
||||
Args:
|
||||
oldNumber: (str) the number the contact previously used
|
||||
newNumber: (str) the new number of the contact
|
||||
"""
|
||||
pass
|
||||
|
||||
def sendEntity(self, entity):
|
||||
"""Sends an entity down the stack (as if YowsupAppLayer called toLower)"""
|
||||
self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT,
|
||||
entity = entity
|
||||
))
|
||||
|
||||
def sendIq(self, iq, onSuccess = None, onError = None):
|
||||
self.stack.broadcastEvent(
|
||||
YowLayerEvent(
|
||||
YowsupAppLayer.SEND_IQ,
|
||||
iq = iq,
|
||||
success = onSuccess,
|
||||
failure = onError,
|
||||
)
|
||||
)
|
||||
|
||||
from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback
|
||||
|
||||
class YowsupAppLayer(YowInterfaceLayer):
|
||||
EVENT_START = 'transwhat.event.YowsupAppLayer.start'
|
||||
TO_LOWER_EVENT = 'transwhat.event.YowsupAppLayer.toLower'
|
||||
SEND_IQ = 'transwhat.event.YowsupAppLayer.sendIq'
|
||||
|
||||
def onEvent(self, layerEvent):
|
||||
# We cannot pass instance varaibles in through init, so we use an event
|
||||
# instead
|
||||
# Return False if you want the event to propogate down the stack
|
||||
# return True otherwise
|
||||
if layerEvent.getName() == YowsupAppLayer.EVENT_START:
|
||||
self.caller = layerEvent.getArg('caller')
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
return True
|
||||
elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED:
|
||||
self.caller.onDisconnect()
|
||||
return True
|
||||
elif layerEvent.getName() == YowsupAppLayer.TO_LOWER_EVENT:
|
||||
self.toLower(layerEvent.getArg('entity'))
|
||||
return True
|
||||
elif layerEvent.getName() == YowsupAppLayer.SEND_IQ:
|
||||
iq = layerEvent.getArg('iq')
|
||||
success = layerEvent.getArg('success')
|
||||
failure = layerEvent.getArg('failure')
|
||||
self._sendIq(iq, success, failure)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ProtocolEntityCallback('success')
|
||||
def onAuthSuccess(self, entity):
|
||||
# entity is SuccessProtocolEntity
|
||||
status = entity.location
|
||||
kind = "" #entity.kind
|
||||
creation = entity.creation
|
||||
expiration = "" #entity.expiration
|
||||
props = entity.props
|
||||
nonce = "" #entity.nonce
|
||||
t = entity.t # I don't know what this is
|
||||
self.caller.onAuthSuccess(status, kind, creation, expiration, props, nonce, t)
|
||||
|
||||
@ProtocolEntityCallback('failure')
|
||||
def onAuthFailure(self, entity):
|
||||
# entity is FailureProtocolEntity
|
||||
reason = entity.reason
|
||||
self.caller.onAuthFailure(reason)
|
||||
|
||||
@ProtocolEntityCallback('receipt')
|
||||
def onReceipt(self, entity):
|
||||
"""Sends ack automatically"""
|
||||
# entity is IncomingReceiptProtocolEntity
|
||||
#ack = OutgoingAckProtocolEntity(entity.getId(),
|
||||
# 'receipt', entity.getType(), entity.getFrom())
|
||||
#self.toLower(entity.ack())
|
||||
_id = entity._id
|
||||
_from = entity._from
|
||||
timestamp = entity.timestamp
|
||||
type = entity.type
|
||||
participant = entity.participant
|
||||
offline = entity.offline
|
||||
items = entity.items
|
||||
self.caller.onReceipt(_id, _from, timestamp, type, participant, offline, items)
|
||||
|
||||
@ProtocolEntityCallback('ack')
|
||||
def onAck(self, entity):
|
||||
# entity is IncomingAckProtocolEntity
|
||||
self.caller.onAck(
|
||||
entity._id,
|
||||
entity._class,
|
||||
entity._from,
|
||||
entity.timestamp
|
||||
)
|
||||
|
||||
@ProtocolEntityCallback('notification')
|
||||
def onNotification(self, entity):
|
||||
"""
|
||||
Sends ack automatically
|
||||
"""
|
||||
self.logger.debug("Received notification (%s): %s" % (type(entity), entity))
|
||||
self.toLower(entity.ack())
|
||||
if isinstance(entity, CreateGroupsNotificationProtocolEntity):
|
||||
self.caller.onAddedToGroup(entity)
|
||||
elif isinstance(entity, AddGroupsNotificationProtocolEntity):
|
||||
self.caller.onParticipantsAddedToGroup(entity)
|
||||
elif isinstance(entity, RemoveGroupsNotificationProtocolEntity):
|
||||
self.caller.onParticipantsRemovedFromGroup(
|
||||
entity.getGroupId().split('@')[0],
|
||||
entity.getParticipants().keys()
|
||||
)
|
||||
elif isinstance(entity, SubjectGroupsNotificationProtocolEntity):
|
||||
self.caller.onSubjectChanged(
|
||||
entity.getGroupId().split('@')[0],
|
||||
entity.getSubject(),
|
||||
entity.getSubjectOwner(full=False),
|
||||
entity.getSubjectTimestamp()
|
||||
)
|
||||
elif isinstance(entity, StatusNotificationProtocolEntity):
|
||||
self.caller.onContactStatusChanged(
|
||||
entity._from.split('@')[0],
|
||||
entity.status
|
||||
)
|
||||
elif isinstance(entity, SetPictureNotificationProtocolEntity):
|
||||
self.caller.onContactPictureChanged(entity.setJid.split('@')[0])
|
||||
elif isinstance(entity, DeletePictureNotificationProtocolEntity):
|
||||
self.caller.onContactPictureChanged(entity.deleteJid.split('@')[0])
|
||||
elif isinstance(entity, RemoveContactNotificationProtocolEntity):
|
||||
self.caller.onContactRemoved(entity.contactJid.split('@')[0])
|
||||
elif isinstance(entity, AddContactNotificationProtocolEntity):
|
||||
self.caller.onContactAdded(
|
||||
entity.contactJid.split('@')[0],
|
||||
entity.notify
|
||||
)
|
||||
elif isinstance(entity, UpdateContactNotificationProtocolEntity):
|
||||
self.caller.onContactUpdated(
|
||||
entity._from.split('@')[0],
|
||||
entity.contactJid.split('@')[0],
|
||||
)
|
||||
|
||||
@ProtocolEntityCallback('message')
|
||||
def onMessageReceived(self, entity):
|
||||
self.logger.debug("Received Message: %s" % unicode(entity))
|
||||
if entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_TEXT:
|
||||
self.caller.onTextMessage(
|
||||
entity._id,
|
||||
entity._from,
|
||||
entity.to,
|
||||
entity.notify,
|
||||
entity.timestamp,
|
||||
entity.participant,
|
||||
entity.offline,
|
||||
entity.retry,
|
||||
entity.body
|
||||
)
|
||||
elif entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_MEDIA:
|
||||
if isinstance(entity, ImageDownloadableMediaMessageProtocolEntity):
|
||||
# There is just way too many fields to pass them into the
|
||||
# function
|
||||
self.caller.onImage(entity)
|
||||
elif isinstance(entity, AudioDownloadableMediaMessageProtocolEntity):
|
||||
self.caller.onAudio(entity)
|
||||
elif isinstance(entity, VideoDownloadableMediaMessageProtocolEntity):
|
||||
self.caller.onVideo(entity)
|
||||
elif isinstance(entity, VCardMediaMessageProtocolEntity):
|
||||
self.caller.onVCard(
|
||||
entity._id,
|
||||
entity._from,
|
||||
entity.name,
|
||||
entity.card_data,
|
||||
entity.to,
|
||||
entity.notify,
|
||||
entity.timestamp,
|
||||
entity.participant
|
||||
)
|
||||
elif isinstance(entity, LocationMediaMessageProtocolEntity):
|
||||
self.caller.onLocation(entity)
|
||||
|
||||
@ProtocolEntityCallback('presence')
|
||||
def onPresenceReceived(self, presence):
|
||||
_type = presence.getType()
|
||||
name = presence.getName()
|
||||
_from = presence.getFrom()
|
||||
last = presence.getLast()
|
||||
self.caller.onPresenceReceived(_type, name, _from, last)
|
||||
|
||||
@ProtocolEntityCallback('chatstate')
|
||||
def onChatstate(self, chatstate):
|
||||
number = chatstate._from.split('@')[0]
|
||||
if chatstate.getState() == ChatstateProtocolEntity.STATE_TYPING:
|
||||
self.caller.onContactTyping(number)
|
||||
else:
|
||||
self.caller.onContactPaused(number)
|
70
transwhat.py
70
transwhat.py
|
@ -1,70 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import asyncore
|
||||
import sys, os
|
||||
import MySQLdb
|
||||
import e4u
|
||||
import threading
|
||||
|
||||
sys.path.insert(0, os.getcwd())
|
||||
|
||||
from Spectrum2.iochannel import IOChannel
|
||||
|
||||
from whatsappbackend import WhatsAppBackend
|
||||
from constants import *
|
||||
|
||||
# Arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--debug', action='store_true')
|
||||
parser.add_argument('--host', type=str, required=True)
|
||||
parser.add_argument('--port', type=int, required=True)
|
||||
parser.add_argument('--service.backend_id', metavar="ID", type=int, required=True)
|
||||
parser.add_argument('config', type=str)
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
# Logging
|
||||
logging.basicConfig( \
|
||||
format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \
|
||||
level = logging.DEBUG if args.debug else logging.INFO \
|
||||
)
|
||||
|
||||
# Handler
|
||||
def handleTransportData(data):
|
||||
plugin.handleDataRead(data)
|
||||
|
||||
e4u.load()
|
||||
|
||||
# Main
|
||||
db = MySQLdb.connect(DB_HOST, DB_USER, DB_PASS, DB_TABLE)
|
||||
io = IOChannel(args.host, args.port, handleTransportData)
|
||||
|
||||
plugin = WhatsAppBackend(io, db)
|
||||
|
||||
asyncore.loop(1)
|
63
utils.py
63
utils.py
|
@ -1,63 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import urllib
|
||||
import json
|
||||
import e4u
|
||||
import base64
|
||||
|
||||
def shorten(url):
|
||||
url = urllib.urlopen("http://d.0l.de/add.json?type=URL&rdata=%s" % urllib.quote(url))
|
||||
response = url.read()
|
||||
response = json.loads(response)
|
||||
|
||||
for entry in response:
|
||||
if entry['type'] == 'success':
|
||||
host = entry['data'][0]['host']
|
||||
return "http://s.%s/%s" % (host['zone']['name'], host['punycode'])
|
||||
|
||||
|
||||
def ago(secs):
|
||||
periods = ["second", "minute", "hour", "day", "week", "month", "year", "decade"]
|
||||
lengths = [60, 60, 24, 7,4.35, 12, 10]
|
||||
|
||||
j = 0
|
||||
diff = secs
|
||||
|
||||
while diff >= lengths[j]:
|
||||
diff /= lengths[j]
|
||||
diff = round(diff)
|
||||
j += 1
|
||||
|
||||
period = periods[j]
|
||||
if diff > 1: period += "s"
|
||||
|
||||
return "%d %s ago" % (diff, period)
|
||||
|
||||
def softToUni(message):
|
||||
message = message.decode("utf-8")
|
||||
return e4u.translate(message, reverse=False, **e4u.SOFTBANK_TRANSLATE_PROFILE)
|
||||
|
||||
def decodePassword(password):
|
||||
return base64.b64decode(bytes(password.encode("utf-8")))
|
|
@ -1,122 +0,0 @@
|
|||
__author__ = "Steffen Vogel"
|
||||
__copyright__ = "Copyright 2013, Steffen Vogel"
|
||||
__license__ = "GPLv3"
|
||||
__maintainer__ = "Steffen Vogel"
|
||||
__email__ = "post@steffenvogel.de"
|
||||
__status__ = "Prototype"
|
||||
|
||||
"""
|
||||
This file is part of transWhat
|
||||
|
||||
transWhat 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
|
||||
any later version.
|
||||
|
||||
transwhat 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 transWhat. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from Spectrum2.backend import SpectrumBackend
|
||||
from Spectrum2 import protocol_pb2
|
||||
|
||||
from session import Session
|
||||
|
||||
import logging
|
||||
|
||||
class WhatsAppBackend(SpectrumBackend):
|
||||
def __init__(self, io, db):
|
||||
SpectrumBackend.__init__(self)
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.io = io
|
||||
self.db = db
|
||||
self.sessions = { }
|
||||
|
||||
self.logger.debug("Backend started")
|
||||
|
||||
# RequestsHandlers
|
||||
def handleLoginRequest(self, user, legacyName, password, extra):
|
||||
self.logger.debug("handleLoginRequest(user=%s, legacyName=%s)", user, legacyName)
|
||||
if user not in self.sessions:
|
||||
self.sessions[user] = Session(self, user, legacyName, extra, self.db)
|
||||
|
||||
self.sessions[user].login(password)
|
||||
|
||||
def handleLogoutRequest(self, user, legacyName):
|
||||
self.logger.debug("handleLogoutRequest(user=%s, legacyName=%s)", user, legacyName)
|
||||
if user in self.sessions:
|
||||
self.sessions[user].logout()
|
||||
del self.sessions[user]
|
||||
|
||||
def handleMessageSendRequest(self, user, buddy, message, xhtml = ""):
|
||||
self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s)", user, buddy, message)
|
||||
self.sessions[user].sendMessageToWA(buddy, message)
|
||||
|
||||
def handleJoinRoomRequest(self, user, room, nickname, pasword):
|
||||
self.logger.debug("handleJoinRoomRequest(user=%s, room=%s, nickname=%s)", user, room, nickname)
|
||||
self.sessions[user].joinRoom(room, nickname)
|
||||
|
||||
def handleStatusChangeRequest(self, user, status, statusMessage):
|
||||
self.logger.debug("handleStatusChangeRequest(user=%s, status=%d, statusMessage=%s)", user, status, statusMessage)
|
||||
self.sessions[user].changeStatusMessage(statusMessage)
|
||||
self.sessions[user].changeStatus(status)
|
||||
|
||||
def handleBuddyUpdatedRequest(self, user, buddy, nick, groups):
|
||||
self.logger.debug("handleBuddyUpdatedRequest(user=%s, buddy=%s, nick=%s, groups=%s)", user, buddy, nick, str(groups))
|
||||
self.sessions[user].updateBuddy(buddy, nick, groups)
|
||||
|
||||
def handleBuddyRemovedRequest(self, user, buddy, groups):
|
||||
self.logger.debug("handleBuddyRemovedRequest(user=%s, buddy=%s, groups=%s)", user, buddy, str(groups))
|
||||
self.sessions[user].removeBuddy(buddy)
|
||||
|
||||
def handleTypingRequest(self, user, buddy):
|
||||
self.logger.debug("handleTypingRequest(user=%s, buddy=%s)", user, buddy)
|
||||
self.sessions[user].sendTypingStarted(buddy)
|
||||
|
||||
def handleTypedRequest(self, user, buddy):
|
||||
self.logger.debug("handleTypedRequest(user=%s, buddy=%s)", user, buddy)
|
||||
self.sessions[user].sendTypingStopped(buddy)
|
||||
|
||||
def handleStoppedTypingRequest(self, user, buddy):
|
||||
self.logger.debug("handleStoppedTypingRequest(user=%s, buddy=%s)", user, buddy)
|
||||
self.sessions[user].sendTypingStopped(buddy)
|
||||
|
||||
# TODO
|
||||
def handleBuddyBlockToggled(self, user, buddy, blocked):
|
||||
pass
|
||||
|
||||
def handleLeaveRoomRequest(self, user, room):
|
||||
pass
|
||||
|
||||
def handleVCardRequest(self, user, buddy, ID):
|
||||
pass
|
||||
|
||||
def handleVCardUpdatedRequest(self, user, photo, nickname):
|
||||
pass
|
||||
|
||||
def handleAttentionRequest(self, user, buddy, message):
|
||||
pass
|
||||
|
||||
def handleFTStartRequest(self, user, buddy, fileName, size, ftID):
|
||||
pass
|
||||
|
||||
def handleFTFinishRequest(self, user, buddy, fileName, size, ftID):
|
||||
pass
|
||||
|
||||
def handleFTPauseRequest(self, ftID):
|
||||
pass
|
||||
|
||||
def handleFTContinueRequest(self, ftID):
|
||||
pass
|
||||
|
||||
def handleRawXmlRequest(self, xml):
|
||||
pass
|
||||
|
||||
def sendData(self, data):
|
||||
self.io.sendData(data)
|
||||
|
Loading…
Reference in New Issue