diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..6de83601 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,21 @@ +Make sure these boxes are checked before submitting your issue -- thank you! + + +- [ ] You are using PHP > 5.6 +- [ ] You have installed all extensions and modules listed in [here](https://github.com/mgp25/Chat-API/wiki/Dependencies). PHP Protobuf and Curve25519 are required! +- [ ] You have checked if someone has already asked the same issue you have. +- [ ] You have read the [wiki](https://github.com/mgp25/Chat-API/wiki). +- [ ] Is not a programming question. +- [ ] You are using latest Chat API code. + + +### Error +Post your error below: + + +### Debug log +Post your debug log below: + +```xml +YOUR DEBUG LOG HERE +``` diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9e88f8c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/bootstrap/compiled.php +/vendor +/.idea +/nbproject +composer.phar +.env.*.php +.env.php +.DS_Store +Thumbs.db +*.dat + +*.db + +*.log diff --git a/EVENTS.md b/EVENTS.md deleted file mode 100755 index 5c02ecc0..00000000 --- a/EVENTS.md +++ /dev/null @@ -1,47 +0,0 @@ -Available events and arguments -============================== -See events/WhatsAppEventListener.php. - -How to bind a callback to an event -================================== - -# Create a WhatsAppEventListener class and implement the method you -# would like to handle: -```php -require 'events/WhatsAppEventListenerBase.php'; - -class MyEventListener extends WhatsAppEventListenerBase { - function onGetMessage( - $phone, // The user phone number including the country code. - $from, // The sender JID. - $msgid, // The message id. - $type, // The message type. - $time, // The unix time when send message notification. - $name, // The sender name. - $message // The message. - ) { - print( "onGetMessage(" . $phone . ", " . $from . ", " . $msgid . ", " . $type . ", " . $time . ", " . $name . ", " . $message . ")\n" ); - } -} -``` -# Require your new class; -```php -require 'MyEventListener.php'; -``` -# Create an instance of WhastProt. -```php -$w = new WhatsProt($userPhone, $userIdentity, $userName, $debug); -``` -# Add your event listener. -```php -w->eventManager()->addEventListener(new MyEventListener()); -``` -# Connect to WhatsApp servers. -```php -$w->connect(); -``` -# Login to WhatsApp -```php -$w->loginWithPassword($password); -``` -[...] diff --git a/LICENSE b/LICENSE index bcfbe6d6..6b156fe1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,675 @@ -The MIT License (MIT) - -Copyright (c) 2014 mgp25 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {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 . + +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: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 59a6eec3..f52d41c2 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,36 @@ -# WhatsAPI +# Chat API [![Latest Stable Version](https://poser.pugx.org/whatsapp/chat-api/v/stable)](https://packagist.org/packages/whatsapp/chat-api) [![Total Downloads](https://poser.pugx.org/whatsapp/chat-api/downloads)](https://packagist.org/packages/whatsapp/chat-api) [![License](https://poser.pugx.org/whatsapp/chat-api/license)](https://packagist.org/packages/whatsapp/chat-api) Interface to WhatsApp Messenger ----------- - -### Update March 15th, 2014 - -Sources are back after brief downtime due to [DCMA takedown](https://github.com/github/dmca/blob/master/2014-02-12-WhatsApp.md). - -### Note July 30th, 2013 -*New policy:* - -*I no longer provide support to users who are trying to send bulk messages using this API (i.e. a large amount of messages and not the built-in bulk message functionality).* -*Sending advertisments on WhatsApp goes directly against their EULA and I have no way of determining whether the user is trying to send spam, advertising or sending mass messages to "opt-in users".* -*And I also don't want to waste the little spare time that I have on trying to figure out ways to fuck up this beautiful ad-free platform called WhatsApp by enabling people to send spam.* -*Everyone is free to use this API but there will be no more issue reports about being blocked after sending messages to semi-random users.* - -*In the famous words of Heath Ledger as the Joker (taken completely out of context by me):* - -**It's not about the money, it's about sending a message.** - -*\- [shirioko](https://github.com/shirioko)* - ----------- +**Read the [wiki](https://github.com/mgp25/Chat-API/wiki)** and previous issues before opening a new one! Maybe your issue is already answered. -### Note July 14th, 2013 -*Events renamed:* -- *A large number of events have been renamed in the event handling system to better match the recent method names.* -- *All event names and parameters have been listed in the EVENTS.md file* +For new WhatsApp updates check **[WhatsApp incoming updates log](https://github.com/mgp25/Chat-API/wiki/WhatsApp-incoming-updates)** +**Do you like this project? Support it by donating** +- ![Paypal](https://raw.githubusercontent.com/reek/anti-adblock-killer/gh-pages/images/paypal.png) Paypal: [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YNVNPLE45DNG6) +- ![btc](https://camo.githubusercontent.com/4bc31b03fc4026aa2f14e09c25c09b81e06d5e71/687474703a2f2f7777772e6d6f6e747265616c626974636f696e2e636f6d2f696d672f66617669636f6e2e69636f) Bitcoin: 1DCEpC9wYXeUGXS58qSsqKzyy7HLTTXNYe ---------- +### Installation -### Note July 10th, 2013 -*Another massive overhaul in the code:* -- *MAJOR RENAMING OF MOST METHODS!! Old legacy code will break, we are sorry but it is necessary to provide a cleaner interface. Please check the new code.* -- *Methods renamed to give a more consistent feel to the API - all methods are now camelCase watch out for typo's!* -- *Initial movement towards bringing the code into alignment with PSR-2 (http://www.php-fig.org/psr/2/)* -- *There is absolutely NO, NONE, NADA, ZIP, 100% FREE of any need to use/enter a MAC address or IMEI in this code. DO NOT TRY!* -- *[New Android token used](https://github.com/karolsarnacki/whatsapp/commit/55d8233b852ecd9f6a6f845586e91e6fadbd0c44#L1L20) as WP7 one appears to no longer work. Long live the WP7 token?* - ----------- +```sh +composer require whatsapp/chat-api +``` +- **Requires:** [PHP Protobuf](https://github.com/allegro/php-protobuf) and [Curve25519](https://github.com/mgp25/curve25519-php) to enable end to end encryption -### Note June 18th, 2013 +### Special thanks -*Big overhaul in the code. Big thanks to:* -- *[Ali Hubail](https://github.com/hubail) and* -- *[Ahmed Moh'd](http://fb.com/ahmed.mhd) for making this project happen (and adding me as a member)* -- *[Jannik Vogel](https://github.com/JayFoxRox) for helping me retrieve the latest WhatsApp token, someone should write a book about it some day..* -- *[Tarek Galal](https://github.com/tgalal) for providing the latest WhatsApp functionality in yowsup* -- *[Atans](https://github.com/atans) and* -- *[Jonathan Williamson](https://github.com/jonnywilliamson) for additional fixes* +- [CODeRUS](https://github.com/CODeRUS) +- [tgalal](https://github.com/tgalal) +- [SikiFn](https://github.com/SikiFn) +- [0xTryCatch](https://github.com/0xTryCatch) +- [Shirioko](https://github.com/shirioko) +- [sinjuice](https://github.com/sinjuice) -*\- [shirioko](https://github.com/shirioko)* +Also Ahmed Moh'd ([fb.com/ahmed.mhd](fb.com/ahmed.mhd)) and Ali Hubail ([@hubail](https://twitter.com/hubail)) for making this project possible. +And everyone that contributes to it. ---------- @@ -63,72 +39,25 @@ According to [the company](http://www.whatsapp.com/): > “WhatsApp Messenger is a cross-platform mobile messenger that replaces SMS and works through the existing internet data plan of your device. WhatsApp is available for iPhone, BlackBerry, Android, Windows Phone, Nokia Symbian60 & S40 phones. Because WhatsApp Messenger uses the same internet data plan that you use for email and web browsing, there is no cost to message and stay in touch with your friends.” -Late 2011 numbers: 1 billion messages per day, ~20 million users. - -### Modified XMPP -WhatsApp uses some sort of customized XMPP server, named internally as FunXMPP, which is basically some extended proprietary version. - -### Login procedure -Much like XMPP, WhatsApp uses JID (jabber id) and password to successfully login to the service. The password is generated by the server and received upon registration. - - -The JID is a concatenation between your country’s code and mobile number. - -Initial login uses Digest Access Authentication. - -### Message sending -Messages are basically sent as TCP packets, following WhatsApp’s own format (unlike what’s defined in XMPP RFCs). +Jan. 2015: 30 billion messages per day, ~700 million users. -Messages are application level encrypted using RC4 keystreams - -### Multimedia Message sending -Photos, Videos and Audio files shared with WhatsApp contacts are HTTP-uploaded to a server before being sent to the recipient(s) along with Base64 thumbnail of media file (if applicable) along with the generated HTTP link as the message body. - -### Event system -WhatsApi uses an event manager (created by [facine](https://github.com/facine)) which allows you to respond to certain events. - -List of events and example code on how to bind an event handler: -https://github.com/shirioko/WhatsAPI/wiki/WhatsApi-events - -# FAQ - - -- **What’s with the hex chars floating all over the code?** - - Mostly WhatsApp’s proprietary control chars/commands, or formatted data according to their server’s specifications, stored in predefined dictionaries within the clients. - -- **What’s your future development plans?** - - We don’t have any. - -- **Would it run over the web?** - - We’ve tested a slightly-modified version on top of Tornado Web Server and worked like a charm, however, building a chat client is a bit tricky, do your research. - -- **Can I receive chats?** - - Indeed, using the same socket-receiving mechanism. But you have to parse the incoming data. Parsing functions aren’t included in this release, maybe in the next one? - -- **I think the code is messy.** - - It’s working. - -- **How can I obtain my password?** +# License - Register a number using WhatsAPI or intercept your phone's password using MissVenom +As of November 1, 2015 Chat API is licensed under the GPLv3+: http://www.gnu.org/licenses/gpl-3.0.html. -# NOTES +# Terms and conditions -- This proof of concept is extensible to contain every feature that make a fully-fledged client, similar to the official ones, actually could be even better. +- You will NOT use this API for marketing purposes (spam, massive sending...). +- We do NOT give support to anyone that wants this API to send massive messages or similar. +- We reserve the right to block any user of this repository that does not meet these conditions. -- During the two weeks of analysis of service mechanisms, we stumbled upon serious design and security flaws (they fixed some of them since 2011). For a company with such massive user base, we expected better practises and engineering. +## Legal -# License +This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by WhatsApp or any of its affiliates or subsidiaries. This is an independent and unofficial API. Use at your own risk. -MIT - refer to the source code for the extra line. -# Venomous +##Cryptography Notice -Team of Bahraini Developers. +This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See http://www.wassenaar.org/ for more information. -Ahmed Moh'd ([fb.com/ahmed.mhd](https://www.facebook.com/ahmed.mhd)) and Ali Hubail ([@hubail](https://twitter.com/hubail)) contributed to this release. +The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. diff --git a/classesMD5.txt b/classesMD5.txt deleted file mode 100644 index f8d11075..00000000 --- a/classesMD5.txt +++ /dev/null @@ -1,110 +0,0 @@ -=============================== -= CLASSES MD5 = -= B64 ENCODED = -=============================== - - -WhatsApp/2.11.407 Android/4.3 Device/GalaxyS3 -xOyKd7AoN0uoos7Fkeup5w== - -WhatsApp/2.11.404 Android/4.3 Device/GalaxyS3 -UwR/p2qFH6trUCtAwVFvlw== - -WhatsApp/2.11.395 Android/4.3 Device/GalaxyS3 [*] Safe version -P3b9TfNFCkkzPoVzZnm+BA== - -WhatsApp/2.11.394 Android/4.3 Device/GalaxyS3 -EkA+wIj+E8R7ncRihvTbJQ== - -WhatsApp/2.11.393 Android/4.3 Device/GalaxyS3 -39ykQ+QRBQ3V4y5u7qnqDA== - -WhatsApp/2.11.391 Android/4.3 Device/GalaxyS3 -08io6i/9AExMEsEar18G5w== - -WhatsApp/2.11.390 Android/4.3 Device/GalaxyS3 -ueXcdj5E56OZ0ABxU/YasQ== - -WhatsApp/2.11.388 Android/4.3 Device/GalaxyS3 -KSb52iVfhyJMXxuGse8avw== - -WhatsApp/2.11.383 Android/4.3 Device/GalaxyS3 -2IVWfReKm2m+1ORLg3gQtg== - -WhatsApp/2.11.382 Android/4.3 Device/GalaxyS3 -IJWjujh8Eds1Ew1pjWZWvg== - -WhatsApp/2.11.380 Android/4.3 Device/GalaxyS3 -b9ztDlCAK+k27UGaZkM7nw== - -WhatsApp/2.11.378 Android/4.3 Device/GalaxyS3 [*] 5 Sept 2014. PS. -oCtjlSonS+4H16h9HW6nNA== - -WhatsApp/2.11.375 Android/4.3 Device/GalaxyS3 -ZLvs/rzS8qU90wZLE/1MgQ== - -WhatsApp/2.11.372 Android/4.3 Device/GalaxyS3 -eAaY/5A1G1nadV0TLtmdFQ== - -WhatsApp/2.11.371 Android/4.3 Device/GalaxyS3 -ScUKonjwWeLYhKqH8uV4HA== - -WhatsApp/2.11.365 Android/4.3 Device/GalaxyS3 -GUfilO0O5J4ZEMWcQKN7mg== - -WhatsApp/2.11.355 Android/4.3 Device/GalaxyS3 -w5mQTrb0Tky4s6Ln6RFMVQ== - -WhatsApp/2.11.354 Android/4.3 Device/GalaxyS3 -MPLa6ffv7Z/WNGS60PpxJQ== - -WhatsApp/2.11.348 Android/4.3 Device/GalaxyS3 -vszYr764HF2FQpdbqTdr/w== - -WhatsApp/2.11.339 Android/4.0.4 Device/GalaxyS3 -iV15qOB/jPIidogqfJ/oJA== - -WhatsApp/2.11.301 Android/4.3 Device/GalaxyS3 -pZ3J/O+F3HXOyx8YixzvPQ== - -WhatsApp/2.11.272 Android/4.0.4 Device/GalaxyS2 -kI6dipxOAk055AWZxknGqg== - -WhatsApp/2.11.264 Android/4.0.4 Device/GalaxyS2 -opf5/xMP2qdqHJ+7d4/LwA== - -WhatsApp/2.11.254 Android/4.0.4 Device/GalaxyS2 -ueKiLIlXuHtGzw2oxoZvfg== - -WhatsApp/2.11.224 Android/4.4.2 Device/GalaxyS4 -ZGdhtLiFqomD5ovSpQ+Odw== - -WhatsApp/2.11.209 Android/4.4.2 Device/GalaxyS4 -+XW/7rCZDX9T7YrGQqTmcg== - -WhatsApp/2.11.200 Android/4.4.2 Device/GalaxyS4 -8PZch1s/VRHIKujxCxOXig== - -WhatsApp/2.11.151 Android/4.2.1 Device/GalaxyS3 -94bjoO7brhy/QJZRceJHYw== - -WhatsApp/2.11.144 Android/4.2.1 Device/GalaxyS3 -QtHrjQzwQjujOOOd1oObgQ== - -WhatsApp/2.11.139 Android/4.2.1 Device/GalaxyS3 -rzoCoMEYKQ6zoUWINKC9oQ== - -WhatsApp/2.11.134 Android/4.2.1 Device/GalaxyS3 -r4WQV17nVTl3+uFlF9mvEg== - -WhatsApp/2.11.125 Android/4.2.1 Device/GalaxyS3 -JqOGtSEKUWEu/jxzCB6Bdw== - -WhatsApp/2.11.113 Android/4.2.1 Device/GalaxyS3 -MOpsiNsR+nHEv0dFc3dqmA== - -WhatsApp/2.11.102 Android/4.2.1 Device/GalaxyS3 -zo7YXXvrrRqpikOi/CveTw== - -WhatsApp/2.11.69 Android/4.2.1 Device/GalaxyS3 -30CnAF22oY+2PUD5pcJGqw== diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..af926a6c --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "whatsapp/chat-api", + "description": "The PHP WhatsApp library", + "license": "GPL-3.0+", + "keywords": [ + "WhatsApp", + "WhatsAPI", + "Chat-API" + ], + "authors": [ + { + "role": "Contributors", + "name": "Chat API Contributing Team", + "homepage": "https://github.com/mgp25/Chat-API/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/mgp25/Chat-API/issues", + "wiki": "https://github.com/mgp25/Chat-API/wiki", + "source": "https://github.com/mgp25/Chat-API" + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "require": { + "php": ">=5.6", + "ext-curl": "*", + "ext-gd" : "*", + "ext-mcrypt": "*", + "ext-openssl" : "*", + "ext-pdo" : "*", + "ext-sockets" : "*", + "ext-sqlite3" : "*" + } +} diff --git a/examples/Simple CLI client.php b/examples/Simple CLI client.php index e3b18df0..d43e4e7c 100755 --- a/examples/Simple CLI client.php +++ b/examples/Simple CLI client.php @@ -16,51 +16,53 @@ ////////////////CONFIGURATION/////////////////////// //////////////////////////////////////////////////// -$username = ""; -$password = ""; -$identity = ""; -$nickname = ""; -$debug = false; +$username = ''; +$password = ''; +$nickname = ''; +$debug = false; ///////////////////////////////////////////////////// if ($_SERVER['argv'][1] == null) { - echo "USO: php ".$_SERVER['argv'][0]." \n\nEj: php cliente.php 34123456789\n\n"; + echo 'USAGE: php '.$_SERVER['argv'][0]." \n\nEj: php client.php 34123456789\n\n"; exit(1); } $target = $_SERVER['argv'][1]; function fgets_u($pStdn) { - $pArr = array($pStdn); + $pArr = [$pStdn]; - if (false === ($num_changed_streams = stream_select($pArr, $write = NULL, $except = NULL, 0))) { - print("\$ 001 Socket Error : UNABLE TO WATCH STDIN.\n"); + if (false === ($num_changed_streams = stream_select($pArr, $write = null, $except = null, 0))) { + echo "\$ 001 Socket Error : UNABLE TO WATCH STDIN.\n"; - return FALSE; + return false; } elseif ($num_changed_streams > 0) { return trim(fgets($pStdn, 1024)); } - return null; } -function onPresenceReceived($username, $from, $type) +function onPresenceAvailable($username, $from) { - $dFrom = str_replace(array("@s.whatsapp.net","@g.us"), "", $from); - if($type == "available") - echo "<$dFrom is online>\n\n"; - else - echo "<$dFrom is offline>\n\n"; + $dFrom = str_replace(['@s.whatsapp.net', '@g.us'], '', $from); + echo "<$dFrom is online>\n\n"; +} + +function onPresenceUnavailable($username, $from, $last) +{ + $dFrom = str_replace(['@s.whatsapp.net', '@g.us'], '', $from); + echo "<$dFrom is offline>\n\n"; } echo "[] logging in as '$nickname' ($username)\n"; -$w = new WhatsProt($username, $identity, $nickname, false); +$w = new WhatsProt($username, $nickname, $debug); -$w->eventManager()->bind("onPresence", "onPresenceReceived"); +$w->eventManager()->bind('onPresenceAvailable', 'onPresenceAvailable'); +$w->eventManager()->bind('onPresenceUnavailable', 'onPresenceUnavailable'); $w->connect(); // Nos conectamos a la red de WhatsApp $w->loginWithPassword($password); // Iniciamos sesion con nuestra contraseña echo "[*]Conectado a WhatsApp\n\n"; $w->sendGetServerProperties(); // Obtenemos las propiedades del servidor $w->sendClientConfig(); // Enviamos nuestra configuración al servidor -$sync = array($target); +$sync = [$target]; $w->sendSync($sync); // Sincronizamos el contacto $w->pollMessage(); // Volvemos a poner en cola mensajes $w->sendPresenceSubscription($target); // Nos suscribimos a la presencia del usuario @@ -68,37 +70,37 @@ function onPresenceReceived($username, $from, $type) $pn = new ProcessNode($w, $target); $w->setNewMessageBind($pn); - while (1) { +while (1) { $w->pollMessage(); $msgs = $w->getMessages(); foreach ($msgs as $m) { - # process inbound messages + // process inbound messages //print($m->NodeString("") . "\n"); } - $line = fgets_u(STDIN); - if ($line != "") { - if (strrchr($line, " ")) { - $command = trim(strstr($line, ' ', TRUE)); - } else { - $command = $line; - } - switch ($command) { - case "/query": - $dst = trim(strstr($line, ' ', FALSE)); - echo "[] Interactive conversation with $contact:\n"; - break; - case "/lastseen": - echo "[] Last seen $target: "; - $w->sendGetRequestLastSeen($target); - break; - default: - $w->sendMessage($target , $line); - break; - } + $line = fgets_u(STDIN); + if ($line != '') { + if (strrchr($line, ' ')) { + $command = trim(strstr($line, ' ', true)); + } else { + $command = $line; + } + switch ($command) { + case '/query': + $dst = trim(strstr($line, ' ', false)); + echo "[] Interactive conversation with $contact:\n"; + break; + case '/lastseen': + echo "[] Last seen $target: "; + $w->sendGetRequestLastSeen($target); + break; + default: + $w->sendMessage($target, $line); + break; } + } } -class ProcessNode +class ProcessNode implements NewMsgBindInterface { protected $wp = false; protected $target = false; @@ -109,13 +111,12 @@ public function __construct($wp, $target) $this->target = $target; } - public function process($node) + public function process(\ProtocolNode $node) { $text = $node->getChild('body'); $text = $text->getData(); - $notify = $node->getAttribute("notify"); - - echo "\n- ".$notify.": ".$text." ".date('H:i')."\n"; + $notify = $node->getAttribute('notify'); - } -} + echo "\n- ".$notify.': '.$text.' '.date('H:i')."\n"; + } +} diff --git a/examples/WA CLI Client/client.php b/examples/WA CLI Client/client.php new file mode 100755 index 00000000..eaaf2e51 --- /dev/null +++ b/examples/WA CLI Client/client.php @@ -0,0 +1,687 @@ + PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $db->exec('CREATE TABLE data (`username` TEXT, `password` TEXT, `nickname` TEXT, `login` TEXT)'); + $sql = 'INSERT INTO data (`username`, `password`, `nickname`, `login`) VALUES (:username, :password, :nickname, :login)'; + $query = $db->prepare($sql); + + $query->execute( + [ + ':username' => $argv[1], + ':password' => $argv[2], + ':nickname' => $argv[3], + ':login' => '1', + ] + ); + } +} + +if ((!file_exists($fileName))) { + echo "Welcome to CLI WA Client\n"; + echo "========================\n\n\n"; + echo 'Your number > '; + $number = trim(fgets(STDIN)); + $w = new WhatsProt($number, $nickname, $debug); + + try { + $result = $w->codeRequest('sms'); + } catch (Exception $e) { + echo 'there is an error'.$e; + } + echo "\nEnter sms code you have received > "; + $code = trim(str_replace('-', '', fgets(STDIN))); + try { + $result = $w->codeRegister($code); + } catch (Exception $e) { + echo 'there is an error'; + } + + echo "\nYour nickname > "; + $nickname = trim(fgets(STDIN)); + do { + echo "Is '$nickname' right?\n"; + echo 'yes/no > '; + $right = trim(fgets(STDIN)); + if ($right != 'yes') { + echo "\nYour nickname > "; + $nickname = trim(fgets(STDIN)); + } + } while ($right != 'yes'); + + $db = new \PDO('sqlite:'.$fileName, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $db->exec('CREATE TABLE data (`username` TEXT, `password` TEXT, `nickname` TEXT, `login` TEXT)'); + + $sql = 'INSERT INTO data (`username`, `password`, `nickname`, `login`) VALUES (:username, :password, :nickname, :login)'; + $query = $db->prepare($sql); + + $query->execute( + [ + ':username' => $number, + ':password' => $result->pw, + ':nickname' => $nickname, + ':login' => '1', + ] + ); +} + +$db = new \PDO('sqlite:'.$fileName, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); +$sql = 'SELECT username, password, nickname, login FROM data'; +$row = $db->query($sql); +$result = $row->fetchAll(); +$username = $result[0]['username']; +$password = $result[0]['password']; +$nickname = $result[0]['nickname']; +$login = $result[0]['login']; + +$w = new WhatsProt($username, $nickname, $debug); +$GLOBALS['wa'] = $w; +$w->setMessageStore(new SqliteMessageStore($username)); +$events = new MyEvents($w); +$w->eventManager()->bind('onGetSyncResult', 'onSyncResult'); +$w->eventManager()->bind('onGetRequestLastSeen', 'onGetRequestLastSeen'); +$w->eventManager()->bind('onPresenceAvailable', 'onPresenceAvailable'); +$w->eventManager()->bind('onPresenceUnavailable', 'onPresenceUnavailable'); +$w->eventManager()->bind('onGetImage', 'onGetImage'); +$w->eventManager()->bind('onGetVideo', 'onGetVideo'); +$w->eventManager()->bind('onGetAudio', 'onGetAudio'); + +$w->connect(); +try { + $w->loginWithPassword($password); +} catch (Exception $e) { + echo "Error: $e"; + exit(); +} +echo "\nConnected to WA\n\n"; +if ($login == '1') { + $w->sendGetClientConfig(); + $w->sendGetServerProperties(); + $w->sendGetGroups(); + $w->sendGetBroadcastLists(); + + $sql = 'UPDATE data SET login=?'; + $query = $db->prepare($sql); + $query->execute(['0']); +} +$w->sendGetPrivacyBlockedList(); +$w->sendAvailableForChat($nickname); +$show = true; +global $onlineContacts; +$GLOBALS['online_contacts'] = []; +$GLOBALS['current_contact']; +$poll = 0; +do { + if ($show) { + showContacts(); + $show = false; + } + $poll++; + if ($poll == 10) { + $w->pollMessage(); + $poll = 0; + } + $mainCmd = fgets_u(STDIN); + switch ($mainCmd) { + case '/add': + echo "\nEnter the number you want to add > "; + $numberToAdd = trim(fgets(STDIN)); + do { + echo "\nIs it right yes/no > "; + $check = trim(fgets(STDIN)); + if ($check != 'yes') { + echo "\nEnter the number you want to add > "; + $numberToAdd = trim(fgets(STDIN)); + } + } while ($check != 'yes'); + echo "\nEnter the nickname/name > "; + $nickname = trim(fgets(STDIN)); + $w->sendSync([$numberToAdd], null, 3); + if ($existUser) { + $w->sendPresenceSubscription($numberToAdd); + addContact($numberToAdd, $nickname); + } + break; + case '/delete': + echo "\nEnter the nickname you want to remove > "; + $nickname = trim(fgets(STDIN)); + do { + echo "\nIs it right yes/no > "; + $check = trim(fgets(STDIN)); + if ($check != 'yes') { + echo "\nEnter the nickname you want to remove > "; + $nickname = trim(fgets(STDIN)); + } + } while ($check != 'yes'); + $numberToRemove = findPhoneByNickname($nickname); + $w->sendSync([], [$numberToRemove], 3); + $w->sendPresenceUnsubscription($numberToRemove); + $contactsDB = __DIR__.DIRECTORY_SEPARATOR.'contacts.db'; + $cDB = new \PDO('sqlite:'.$contactsDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $sql = 'DELETE FROM contacts WHERE nickname = :nickname'; + $query = $cDB->prepare($sql); + $query->execute([':nickname' => $nickname]); + break; + case '/contacts': + $show = true; + break; + case '/status': + echo "\nEnter your status > "; + $status = trim(fgets(STDIN)); + do { + echo "\nIs it right yes/no > "; + $check = trim(fgets(STDIN)); + if ($check != 'yes') { + echo "\nEnter your status > "; + $status = trim(fgets(STDIN)); + } + } while ($check != 'yes'); + $w->sendStatusUpdate($status); + break; + case '/profile': + echo "\nEnter your profile picture URL > "; + $profile = trim(fgets(STDIN)); + do { + echo "\nIs it right yes/no > "; + $check = trim(fgets(STDIN)); + if ($check != 'yes') { + echo "\nEnter your profile picture URL > "; + $profile = trim(fgets(STDIN)); + } + } while ($check != 'yes'); + if (!filter_var($profile, FILTER_VALIDATE_URL) === false) { + if (@getimagesize($profile) !== false) { + $w->sendSetProfilePicture($profile); + } + } else { + echo "$profile is NOT a valid URL\n\n"; + } + break; + case '/credits': + echo "\nSpecials thanks to 0xtryCatch :D\n"; + break; + case '/secret': + echo "If you are a spammer or bulk sender, this is your lucky day! Follow the link:\n"; + echo "http://bit.ly/1dOj8e0\n\n"; + echo ":)\n\n"; + exit(); + break; + case '/chat': + echo "\nEnter the name of the contact > "; + $nickname = trim(fgets(STDIN)); + do { + echo "\nIs it right yes/no > "; + $check = trim(fgets(STDIN)); + if ($check != 'yes') { + echo "\nEnter the number you want to add > "; + $nickname = trim(fgets(STDIN)); + } + } while ($check != 'yes'); + + echo "\n\n"; + echo "You are chatting with $nickname\n"; + echo "=================================\n\n"; + $contact = findPhoneByNickname($nickname); + $latestMsgs = getLatestMessages($contact); + $GLOBALS['current_contact'] = $contact; + foreach ($latestMsgs as $msg) { + echo "\n- ".$nickname.': '.$msg['message'].' '.date('t/m/Y h:i:s A', $msg['t'])."\n"; + } + $pn = new ProcessNode($w, $contact); + $w->setNewMessageBind($pn); + $chatting = true; + $compose = true; + $lastSeen = true; + while ($chatting) { + $w->pollMessage(); + $msgs = $w->getMessages(); + foreach ($msgs as $m) { + // process inbound messages + //print($m->NodeString("") . "\n"); + } + if ($compose) { + $w->sendMessageComposing($contact); + $compose = false; + } + if ($lastSeen) { + if (!in_array($contact, $GLOBALS['online_contacts'])) { + $w->sendGetRequestLastSeen($contact); + } + $lastSeen = false; + } + $line = fgets_u(STDIN); + /* + $typing = true; + while (($c = fread(STDIN, 1)) && ($w->pollMessage())) + { + if ($typing) + $w->sendMessageComposing($contact); + switch (ord($c)) { + case 8: + // Backspace + $text = substr($line, 0, -1); + break; + case 10: + // Newline + $line = $text; + $text = ""; + $w->sendMessagePaused($contact); + break 2; + default: + $text .= $c; + break; + } + $typing = false; + } + */ + if ($line != '') { + if (strrchr($line, ' ')) { + $command = trim(strstr($line, ' ', true)); + } else { + $command = $line; + } + switch ($command) { + case '/current': + $nickname = findNicknameByPhone($contact); + echo "[] Interactive conversation with $nickname:\n"; + break; + case '/lastseen': + echo "[] Last seen $contact: "; + $w->sendMessagePaused($contact); + $compose = true; + $w->sendGetRequestLastSeen($contact); + break; + case '/block': + echo "< User is now blocked >\n"; + $w->sendMessagePaused($contact); + $compose = true; + $blocked = privacySettings($contact, 'block'); + $w->sendSetPrivacyBlockedList($blocked); + break; + case '/unblock': + echo "< User is now unblocked >\n"; + $w->sendMessagePaused($contact); + $compose = true; + $blocked = privacySettings($contact, 'unblock'); + $w->sendSetPrivacyBlockedList($blocked); + break; + case '/back': + echo "\nYou are now in the main menu\n"; + echo "================================\n\n"; + $chatting = false; + break; + case '/time': + echo date("l jS \of F Y h:i:s A")."\n\n"; + break; + case '/help': + echo "Available commands\n"; + echo "==================\n\n"; + echo "/query - Shows the number you are chatting with\n"; + echo "/lastseen - Last seen of the user\n"; + echo "/block - Blocks the user\n"; + echo "/unblock - Unblocks user\n"; + echo "/time - Current time\n"; + echo "/back - Return to main menu\n\n"; + break; + default: + $w->sendMessagePaused($contact); + if (!filter_var($line, FILTER_VALIDATE_URL) === false) { + if (@getimagesize($line) !== false) { + $w->sendMessageImage($contact, $line); + } + } else { + $w->sendMessage($contact, $line); + } + $compose = true; + break; + } + } + } + break; + case '/time': + echo date("l jS \of F Y h:i:s A")."\n\n"; + break; + case '/help': + echo "Available commands\n"; + echo "==================\n\n"; + echo "/add - Adds a contact\n"; + echo "/delete - Removes a contact\n"; + echo "/chat - Starts a conversation\n"; + echo "/contacts - Shows all contacts\n"; + echo "/status - Change status\n"; + echo "/profile - Change profile image\n"; + echo "/time - Current time\n"; + echo "/credits - Credits & special thanks\n"; + echo "/exit - Close WA CLI Client\n\n"; + break; + default: + //code + break; + } +} while (($mainCmd != '/exit')); + +$w->disconnect(); +echo "Disconnected. Bye! :D\n"; + +function showContacts() +{ + $contactsDB = __DIR__.DIRECTORY_SEPARATOR.'contacts.db'; + if (file_exists($contactsDB)) { + $cDB = new \PDO('sqlite:'.$contactsDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $sql = 'SELECT nickname FROM contacts'; + $row = $cDB->query($sql); + $contacts = $row->fetchAll(); + echo "\n Contacts\n"; + echo "==================\n\n"; + foreach ($contacts as $contact) { + echo '- '.$contact['nickname']."\n"; + } + echo "\n\n"; + } +} + +function fgets_u($pStdn) +{ + $pArr = [$pStdn]; + + if (false === ($num_changed_streams = stream_select($pArr, $write = null, $except = null, 0))) { + echo "\$ 001 Socket Error : UNABLE TO WATCH STDIN.\n"; + + return false; + } elseif ($num_changed_streams > 0) { + return trim(fgets($pStdn, 1024)); + } +} + +function addContact($number, $name) +{ + $contactsDB = __DIR__.DIRECTORY_SEPARATOR.'contacts.db'; + if (!file_exists($contactsDB)) { + $db = new \PDO('sqlite:'.$contactsDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $db->exec('CREATE TABLE contacts (`phone` TEXT, `nickname` TEXT)'); + } else { + $db = new \PDO('sqlite:'.$contactsDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + } + $sql = 'INSERT INTO contacts (`phone`, `nickname`) VALUES (:phone, :nickname)'; + $query = $db->prepare($sql); + + $query->execute( + [ + ':phone' => $number, + ':nickname' => $name, + ] + ); +} + +function findPhoneByNickname($contact) +{ + $contactsDB = __DIR__.DIRECTORY_SEPARATOR.'contacts.db'; + if (file_exists($contactsDB)) { + $cDB = new \PDO('sqlite:'.$contactsDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $sql = 'SELECT phone FROM contacts WHERE nickname = :nickname'; + $query = $cDB->prepare($sql); + $query->execute([':nickname' => $contact]); + $contact = $query->fetchAll(); + $contact = $contact[0]['phone']; + + return $contact; + } +} + +function findNicknameByPhone($phone) +{ + $contactsDB = __DIR__.DIRECTORY_SEPARATOR.'contacts.db'; + if (file_exists($contactsDB)) { + $cDB = new \PDO('sqlite:'.$contactsDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $sql = 'SELECT nickname FROM contacts WHERE phone = :phone'; + $query = $cDB->prepare($sql); + $query->execute([':phone' => $phone]); + $contact = $query->fetchAll(); + $contact = $contact[0]['nickname']; + + return $contact; + } +} + +function getLatestMessages($phone) +{ + $msgDB = $GLOBALS['msg_db']; + if (file_exists($msgDB)) { + $cDB = new \PDO('sqlite:'.$msgDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $sql = 'SELECT message, t FROM messages WHERE `from` = :phone LIMIT 20'; + $query = $cDB->prepare($sql); + $query->execute([':phone' => $phone]); + $messages = $query->fetchAll(); + + return $messages; + } +} + +function privacySettings($number, $option) +{ + if ($option == 'block') { + $privacyDB = __DIR__.DIRECTORY_SEPARATOR.'privacy.db'; + if (!file_exists($privacyDB)) { + $pDB = new \PDO('sqlite:'.$privacyDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $pDB->exec('CREATE TABLE blocked (`phone` TEXT)'); + } else { + $pDB = new \PDO('sqlite:'.$privacyDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + } + $sql = 'INSERT INTO blocked (`phone`) VALUES (:phone)'; + $query = $pDB->prepare($sql); + + $query->execute( + [ + ':phone' => $number, + ] + ); + + $sql = 'SELECT phone FROM blocked'; + $query = $pDB->prepare($sql); + $query->execute(); + $blocked = $query->fetchAll(); + $i = 0; + for ($i; $i < count($blocked); $i++) { + $blockedList[] = $blocked[$i]['phone']; + } + + return $blockedList; + } else { + $privacyDB = __DIR__.DIRECTORY_SEPARATOR.'privacy.db'; + if (file_exists($privacyDB)) { + $pDB = new \PDO('sqlite:'.$privacyDB, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $sql = 'DELETE FROM blocked WHERE phone = :phone'; + $query = $pDB->prepare($sql); + $query->execute([':phone' => $number]); + + $sql = 'SELECT phone FROM blocked'; + $query = $pDB->prepare($sql); + $query->execute(); + $blocked = $query->fetchAll(); + $i = 0; + for ($i; $i < count($blocked); $i++) { + $blockedList[] = $blocked[$i]['phone']; + } + + return $blockedList; + } + } +} + +function onSyncResult($result) +{ + foreach ($result->existing as $number) { + global $existUser; + $existUser = true; + } +} + +function onGetRequestLastSeen($mynumber, $from, $id, $seconds) +{ + $nickname = findNicknameByPhone(ExtractNumber($from)); + if (($seconds != '') || ($seconds != null)) { + echo "$nickname last seen: ".gmdate('l jS \of F Y h:i:s A', intval($seconds))."\n"; + } +} + +function onPresenceAvailable($mynumber, $from) +{ + $number = ExtractNumber($from); + if (!in_array($number, $GLOBALS['online_contacts'])) { + array_push($GLOBALS['online_contacts'], $number); + } + $nickname = findNicknameByPhone($number); + echo " < $nickname is now online >\n"; +} + +function onPresenceUnavailable($mynumber, $from, $last) +{ + $number = ExtractNumber($from); + if (($key = array_search($number, $GLOBALS['online_contacts'])) !== false) { + unset($GLOBALS['online_contacts'][$key]); + } + $nickname = findNicknameByPhone($number); + echo " < $nickname is now offline >\n"; +} + +function onGetMessage($mynumber, $from, $id, $type, $time, $name, $body) +{ + $number = ExtractNumber($from); + if ($number != $GLOBALS['current_contact']) { + $nickname = findNicknameByPhone($number); + if (($nickname != '') || ($nickname != null)) { + echo " < New message from $nickname >"; + } else { + echo " < New message from $name ($number) >"; + do { + echo "\nDo you want to add $name ($number)? add/block/nothing\n"; + echo '> '; + $option = trim(fgets(STDIN)); + } while (($option != 'add') || ($option != 'block') || ($option != 'nothing')); + + switch ($option) { + case 'add': + echo "\nEnter the nickname/name > "; + $nickname = trim(fgets(STDIN)); + addContact($number, $nickname); + $GLOBALS['wa']->sendSync([$number], null, 3); + $GLOBALS['wa']->sendPresenceSubscription($number); + break; + case 'block': + $blockedContacts = privacySettings($number, 'block'); + $GLOBALS['wa']->sendSetPrivacyBlockedList($blockedContacts); + echo "$name ($number) is now blocked\n"; + break; + } + } + } +} + +function onGetImage($mynumber, $from, $id, $type, $time, $name, $size, $url, $file, $mimeType, $fileHash, $width, $height, $preview, $caption) +{ + $number = ExtractNumber($from); + $nickname = findNicknameByPhone($number); + $path = __DIR__.DIRECTORY_SEPARATOR."data/media/$nickname/"; + if (!file_exists($path)) { + mkdir($path); + } + $filename = $path.time().'.jpg'; + $data = file_get_contents($url); + $fp = @fopen($filename, 'w'); + if ($fp) { + fwrite($fp, $data); + fclose($fp); + } + echo " < Received image from $nickname >\n"; +} + +function onGetVideo($mynumber, $from, $id, $type, $time, $name, $url, $file, $size, $mimeType, $fileHash, $duration, $vcodec, $acodec, $preview, $caption) +{ + $number = ExtractNumber($from); + $nickname = findNicknameByPhone($number); + $path = "data/media/$nickname/"; + if (!file_exists($path)) { + mkdir($path); + } + $filename = __DIR__.DIRECTORY_SEPARATOR.$path.time().'.jpg'; + $data = file_get_contents($url); + $fp = @fopen($filename, 'w'); + if ($fp) { + fwrite($fp, $data); + fclose($fp); + } + echo " < Received video from $nickname >\n"; +} + +function onGetAudio($mynumber, $from, $id, $type, $time, $name, $size, $url, $file, $mimeType, $fileHash, $duration, $acodec, $fromJID_ifGroup = null) +{ + $number = ExtractNumber($from); + $nickname = findNicknameByPhone($number); + $path = "data/media/$nickname/"; + if (!file_exists($path)) { + mkdir($path); + } + $filename = __DIR__.DIRECTORY_SEPARATOR.$path.time().'.jpg'; + $data = file_get_contents($url); + $fp = @fopen($filename, 'w'); + if ($fp) { + fwrite($fp, $data); + fclose($fp); + } + echo " < Received audio from $nickname >\n"; +} + +class ProcessNode implements NewMsgBindInterface +{ + protected $wp = false; + protected $target = false; + + public function __construct($wp, $target) + { + $this->wp = $wp; + $this->target = $target; + } + + public function process(\ProtocolNode $node) + { + if ($node->getAttribute('type') == 'text') { + $text = $node->getChild('body'); + $text = $text->getData(); + $number = ExtractNumber($node->getAttribute('from')); + $nickname = findNicknameByPhone($number); + + echo "\n- ".$nickname.': '.$text.' '.date('H:i')."\n"; + } + } +} diff --git a/examples/WA CLI Client/data/media/.gitkeep b/examples/WA CLI Client/data/media/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/ajaxDemo/ajax.php b/examples/ajaxDemo/ajax.php index d1c3319b..e7990d39 100755 --- a/examples/ajaxDemo/ajax.php +++ b/examples/ajaxDemo/ajax.php @@ -1,31 +1,31 @@ $target, "body" => $message); - $_SESSION["outbound"] = $outbound; + case 'sendMessage': + $target = $_POST['target']; + $message = $_POST['message']; + $outbound = $_SESSION['outbound']; + $outbound[] = ['target' => $target, 'body' => $message]; + $_SESSION['outbound'] = $outbound; break; - case "pollMessages": - $inbound = @$_SESSION["inbound"]; - $_SESSION["inbound"] = array(); - $profilepic = @$_SESSION["profilepic"]; + case 'pollMessages': + $inbound = @$_SESSION['inbound']; + $_SESSION['inbound'] = []; + $profilepic = @$_SESSION['profilepic']; $ret = new JSONResponse(); - if ($profilepic != null && $profilepic != "") { + if ($profilepic != null && $profilepic != '') { $ret->profilepic = $profilepic; } - $_SESSION["profilepic"] = ""; + $_SESSION['profilepic'] = ''; if (count($inbound) > 0) { foreach ($inbound as $message) { $ret->messages[] = $message; @@ -33,4 +33,4 @@ class JSONResponse } echo json_encode($ret); break; -} \ No newline at end of file +} diff --git a/examples/ajaxDemo/index.php b/examples/ajaxDemo/index.php index 5f146c07..da7c8950 100755 --- a/examples/ajaxDemo/index.php +++ b/examples/ajaxDemo/index.php @@ -1,10 +1,10 @@ - - - - - - - - - - - - - - - - -
-

Whatsapp! Messenger

-
- -
-
-
-
- -
-
- - -
-
-
-
- -
-
- - -
-
-
-
- -
-
- - -
-
-
-
- -
-
- -
-
-
-
- -
- -
-
- - -
-
-
-
- -
-
- - -
-
-
-
- -
-
- - -
-
-
-
- -
-
- - -
- - - - -
-
- -
-
-
-
-
-
-
- - -
-
-
-
-
- - + + + + + Personal Whatsapp + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+

Whatsapp! Messenger

+
+
+
+ +
+
+ +
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ + key = null; + } + + public function setKey($key) + { + $this->key = $key; + } + + public function nextTree($input = null) + { + if ($input != null) { + $this->input = $input; + } + $firstByte = $this->peekInt8(); + $stanzaFlag = ($firstByte & 0xF0) >> 4; //ENCRYPTED + /*$isCompressed = (0x400000 & $firstByte) > 0; + $isEncrypted = (0x800000 & $firstByte) > 0;*/ + $stanzaSize = $this->peekInt16(1) | (($firstByte & 0x0F) << 16); + if ($stanzaSize > strlen($this->input)) { + throw new Exception("Incomplete message $stanzaSize != ".strlen($this->input)); + } + + $head = $this->readInt24(); + if ($stanzaFlag & 8) { + if (isset($this->key)) { + $realSize = $stanzaSize - 4; + $this->input = $this->key->DecodeMessage($this->input, $realSize, 0, $realSize); // . $remainingData; + if ($stanzaFlag & 4) { //compressed + $this->input = gzuncompress($this->input); // done + } + } else { + throw new Exception('Encountered encrypted message, missing key'); + } + } + if ($stanzaSize > 0) { + return $this->nextTreeInternal(); + } + } + + protected function readNibble() + { + $byte = $this->readInt8(); + + $ignoreLastNibble = (bool) ($byte & 0x80); + $size = ($byte & 0x7f); + $nrOfNibbles = $size * 2 - (int) $ignoreLastNibble; + + $data = $this->fillArray($size); + $string = ''; + + for ($i = 0; $i < $nrOfNibbles; $i++) { + $byte = $data[(int) floor($i / 2)]; + $ord = ord($byte); + + $shift = 4 * (1 - $i % 2); + $decimal = ($ord & (15 << $shift)) >> $shift; + + switch ($decimal) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + $string .= $decimal; + break; + case 10: + case 11: + $string .= chr($decimal - 10 + 45); + break; + default: + throw new Exception("Bad nibble: $decimal"); + } + } + + return $string; + } + + protected function getToken($token) + { + $ret = ''; + $subdict = false; + TokenMap::GetToken($token, $subdict, $ret); + if (!$ret) { + $token = $this->readInt8(); + TokenMap::GetToken($token, $subdict, $ret); + if (!$ret) { + throw new Exception("BinTreeNodeReader->getToken: Invalid token/length in getToken $token"); + } + } + + return $ret; + } + + protected function getTokenDouble($n, $n2) + { + $pos = $n2 + $n * 256; + $ret = ''; + $subdict = true; + TokenMap::GetToken($pos, $subdict, $ret); + if (!$ret) { + throw new Exception("BinTreeNodeReader->getToken: Invalid token $pos($n + $n * 256)"); + } + + return $ret; + } + + protected function readString($token) + { + $ret = ''; + if ($token == -1) { + throw new Exception("BinTreeNodeReader->readString: Invalid -1 token in readString $token"); + } + + if (($token > 2) && ($token < 236)) { + return $this->getToken($token); + } else { + switch ($token) { + case 0: + $ret = ''; + break; + case 236: + case 237: + case 238: + case 239: + $token2 = $this->readInt8(); + + return $this->getTokenDouble($token - 236, $token2); + break; + case 250: { + $readString = $this->readString($this->readInt8()); + $s = $this->readString($this->readInt8()); + if ($readString != null && $s != null) { + return $readString.'@'.$s; + } + if ($s == null) { + return ''; + } + break; + } + case 251: + case 255: + return $this->readPacked8($token); //maybe utf8 decode + case 252: { + $len = $this->readInt8(); + + return $this->fillArray($len); //maybe ut8 decode + } + case 253: { + $len = $this->readInt20(); + + return $this->fillArray($len); //maybe ut8 decode + } + case 254: { + $len = $this->readInt31(); + + return $this->fillArray($len); //maybe ut8 decode + } + default: + throw new Exception("readString couldn't match token ".$token); + } + } + } + + protected function readPacked8($n) + { + $len = $this->readInt8(); + $remove = 0; + if (($len & 0x80) != 0 && $n == 251) { + $remove = 1; + } + $len = $len & 0x7F; + $text = substr($this->input, 0, $len); + $this->input = substr($this->input, $len); + $data = bin2hex($text); + $len = strlen($data); + $out = ''; + for ($i = 0; $i < $len; ++$i) { + $val = ord(hex2bin('0'.$data[$i])); + if ($i == ($len - 1) && $val > 11 && $n != 251) { + continue; + } + $out .= chr($this->unpackByte($n, $val)); + } + + return substr($out, 0, strlen($out) - $remove); + } + + protected function unpackByte($n, $n2) + { + switch ($n) { + case 251: + return $this->unpackHex($n2); + case 255: + return $this->unpackNibble($n2); + default: + throw new Exception('bad packed type ' + $n); + } + } + + protected function unpackHex($n) + { + switch ($n) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + return $n + 48; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return 65 + ($n - 10); + default: + throw new Exception('bad hex '.$n); + } + } + + protected function unpackNibble($n) + { + switch ($n) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + return $n + 48; + case 10: + case 11: + return 45 + ($n - 10); + default: + throw new Exception('bad nibble '.$n); + } + } + + protected function readAttributes($size) + { + $attributes = []; + $attribCount = ($size - 2 + $size % 2) / 2; + for ($i = 0; $i < $attribCount; $i++) { + $len1 = $this->readInt8(); + $key = $this->readString($len1); + $len2 = $this->readInt8(); + $value = $this->readString($len2); + $attributes[$key] = $value; + } + + return $attributes; + } + + protected function inflateBuffer($stanzaSize = 0) + { + $this->input = gzuncompress($this->input); // maybe gzinflate or gzdecode . + } + + protected function nextTreeInternal() + { + $size = $this->readListSize($this->readInt8()); + $token = $this->readInt8(); + if ($token == 1) { + $token = $this->readInt8(); + } + if ($token == 2) { + return; + } + + $tag = $this->readString($token); + if ($size == 0 || $size == null) { + throw new Exception('nextTree sees 0 list or null tag'); + } + $attributes = $this->readAttributes($size); + if ($size % 2 == 1) { + return new ProtocolNode($tag, $attributes, null, ''); + } + $read2 = $this->readInt8(); + if ($this->isListTag($read2)) { + return new ProtocolNode($tag, $attributes, $this->readList($read2), ''); + } + switch ($read2) { + case 252: + $len = $this->readInt8(); + $data = $this->fillArray($len); //maybe ut8 decode + return new ProtocolNode($tag, $attributes, null, $data); + break; + case 253: + $len = $this->readInt20(); + $data = $this->fillArray($len); //maybe ut8 decode + return new ProtocolNode($tag, $attributes, null, $data); + break; + case 254: + $len = $this->readInt31(); + $data = $this->fillArray($len); //maybe ut8 decode + return new ProtocolNode($tag, $attributes, null, $data); + break; + case 255: + case 251: + return new ProtocolNode($tag, $attributes, null, $this->readPacked8($read2)); + break; + default: + return new ProtocolNode($tag, $attributes, null, $this->readString($read2)); + break; + } + } + + protected function isListTag($token) + { + return $token == 248 || $token == 0 || $token == 249; + } + + protected function readList($token) + { + $size = $this->readListSize($token); + $ret = []; + for ($i = 0; $i < $size; $i++) { + array_push($ret, $this->nextTreeInternal()); + } + + return $ret; + } + + protected function readListSize($token) + { + if ($token == 0) { + return 0; + } + if ($token == 0xf8) { + return $this->readInt8(); + } elseif ($token == 0xf9) { + return $this->readInt16(); + } + + throw new Exception("BinTreeNodeReader->readListSize: invalid list size in readListSize: token $token"); + } + + protected function peekInt24($offset = 0) + { + $ret = 0; + if (strlen($this->input) >= (3 + $offset)) { + $ret = ord(substr($this->input, $offset, 1)) << 16; + $ret |= ord(substr($this->input, $offset + 1, 1)) << 8; + $ret |= ord(substr($this->input, $offset + 2, 1)) << 0; + } + + return $ret; + } + + public function readHeader($offset = 0) + { + $ret = 0; + if (strlen($this->input) >= (3 + $offset)) { + $b0 = ord(substr($this->input, $offset, 1)); + $b1 = ord(substr($this->input, $offset + 1, 1)); + $b2 = ord(substr($this->input, $offset + 2, 1)); + $ret = $b0 + (($b1 << 16) + ($b2 << 8)); + } + + return $ret; + } + + protected function readInt24() + { + $ret = $this->peekInt24(); + if (strlen($this->input) >= 3) { + $this->input = substr($this->input, 3); + } + + return $ret; + } + + protected function peekInt16($offset = 0) + { + $ret = 0; + if (strlen($this->input) >= (2 + $offset)) { + $ret = ord(substr($this->input, $offset, 1)) << 8; + $ret |= ord(substr($this->input, $offset + 1, 1)) << 0; + } + + return $ret; + } + + protected function readInt16() + { + $ret = $this->peekInt16(); + if ($ret > 0) { + $this->input = substr($this->input, 2); + } + + return $ret; + } + + protected function peekInt8($offset = 0) + { + $ret = 0; + if (strlen($this->input) >= (1 + $offset)) { + $sbstr = substr($this->input, $offset, 1); + $ret = ord($sbstr); + } + + return $ret; + } + + protected function readInt8() + { + $ret = $this->peekInt8(); + if (strlen($this->input) >= 1) { + $this->input = substr($this->input, 1); + } + + return $ret; + } + + protected function peekInt20($offset = 0) + { + $ret = 0; + if (strlen($this->input) >= (3 + $offset)) { + $b1 = ord(substr($this->input, $offset, 1)); + $b2 = ord(substr($this->input, $offset + 1, 1)); + $b3 = ord(substr($this->input, $offset + 2, 1)); + $ret = ($b1 << 16) | ($b2 << 8) | $b3; + } + + return $ret; + } + + protected function readInt20() + { + $ret = $this->peekInt20(); + if (strlen($this->input) >= 3) { + $this->input = substr($this->input, 3); + } + + return $ret; + } + + protected function peekInt31($offset = 0) + { + $ret = 0; + if (strlen($this->input) >= (4 + $offset)) { + $b1 = ord(substr($this->input, $offset, 1)); + $b2 = ord(substr($this->input, $offset + 1, 1)); + $b3 = ord(substr($this->input, $offset + 2, 1)); + $b4 = ord(substr($this->input, $offset + 3, 1)); + // $n = 0x7F & $b1; dont know what is this for + $ret = ($b1 << 24) | ($b2 << 16) | ($b3 << 8) | $b4; + } + + return $ret; + } + + protected function readInt31() + { + $ret = $this->peekInt31(); + if (strlen($this->input) >= 4) { + $this->input = substr($this->input, 4); + } + + return $ret; + } + + protected function fillArray($len) + { + $ret = ''; + if (strlen($this->input) >= $len) { + $ret = substr($this->input, 0, $len); + $this->input = substr($this->input, $len); + } + + return $ret; + } +} diff --git a/src/BinTreeNodeWriter.php b/src/BinTreeNodeWriter.php new file mode 100644 index 00000000..a23d5920 --- /dev/null +++ b/src/BinTreeNodeWriter.php @@ -0,0 +1,383 @@ +key = null; + } + + public function setKey($key) + { + $this->key = $key; + } + + /*public function ResetStreamNewStanza(){ + return "\x00\x00\x00"; + }*/ + /*public function NewStreamReset(){ + $this->output = "\x00\x00\x00\x00\x00\x00"; + }*/ + + public function StartStream($domain, $resource) + { + $attributes = []; + $attributes['to'] = $domain; + $attributes['resource'] = $resource; + + $this->writeListStart(count($attributes) * 2 + 1); + + $this->output .= "\x01"; + + $this->writeAttributes($attributes); + //echo "tx "se. "WA" . $this->writeInt8(1) . $this->writeInt8(6)."\n"; + return 'WA'.$this->writeInt8(1).$this->writeInt8(6).$this->flushBuffer(); + } + + /** + * @param ProtocolNode $node + * @param bool $encrypt + * + * @return string + */ + public function write($node, $encrypt = true) + { + if ($node == null) { + $this->output .= "\x00"; + } else { + $this->writeInternal($node); + } + + return $this->flushBuffer($encrypt); + } + + /** + * @param ProtocolNode $node + */ + /*protected function writeInternal($node) + { + $len = 1; + if ($node->getAttributes() != null) { + $len += count($node->getAttributes()) * 2; + } + if (count($node->getChildren()) > 0) { + $len += 1; + } + if (strlen($node->getData()) > 0) { + $len += 1; + } + $this->writeListStart($len); + $this->writeString($node->getTag()); + $this->writeAttributes($node->getAttributes()); + if (strlen($node->getData()) > 0) { + $this->writeBytes($node->getData()); + } + if ($node->getChildren()) { + $this->writeListStart(count($node->getChildren())); + foreach ($node->getChildren() as $child) { + $this->writeInternal($child); + } + } + }*/ + + private function writeInternal($protocolTreeNode) + { + $len = 1; + if ($protocolTreeNode->getAttributes() != null) { + $len += count($protocolTreeNode->getAttributes()) * 2; + } + if (count($protocolTreeNode->getChildren()) > 0) { + $len += 1; + } + if (strlen($protocolTreeNode->getData()) > 0) { + $len += 1; + } + $this->writeListStart($len); + $this->writeString($protocolTreeNode->getTag()); + $this->writeAttributes($protocolTreeNode->getAttributes()); + if (strlen($protocolTreeNode->getData()) > 0) { + $this->writeBytes($protocolTreeNode->getData()); + } + if ($protocolTreeNode->getChildren()) { + $this->writeListStart(count($protocolTreeNode->getChildren())); + foreach ($protocolTreeNode->getChildren() as $child) { + $this->writeInternal($child); + } + } + } + + protected function parseInt24($data) + { + $ret = ord(substr($data, 0, 1)) << 16; + $ret |= ord(substr($data, 1, 1)) << 8; + $ret |= ord(substr($data, 2, 1)) << 0; + + return $ret; + } + + private static function packHex($n) + { + switch ($n) { + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + return $n - 48; + case 65: + case 66: + case 67: + case 68: + case 69: + case 70: + return 10 + ($n - 65); + default: + return -1; + } + } + + private static function packNibble($n) + { + switch ($n) { + case 45: + case 46: + return 10 + ($n - 45); + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + return $n - 48; + default: + return -1; + } + } + + protected function flushBuffer($encrypt = true) + { + $size = strlen($this->output); + $data = $this->output; + if ($this->key != null && $encrypt) { + $bsize = $this->getInt24($size); + $data = $this->key->EncodeMessage($data, $size, 0, $size); + $len = strlen($data); + $bsize[0] = chr((8 << 4) | (($len & 16711680) >> 16)); + $bsize[1] = chr(($len & 65280) >> 8); + $bsize[2] = chr($len & 255); + $size = $this->parseInt24($bsize); + } + $ret = $this->writeInt24($size).$data; + $this->output = ''; + + return $ret; + } + + protected function getInt24($length) + { + $ret = ''; + $ret .= chr((($length & 0xf0000) >> 16)); + $ret .= chr((($length & 0xff00) >> 8)); + $ret .= chr(($length & 0xff)); + + return $ret; + } + + protected function writeToken($token) + { + if ($token <= 255 && $token >= 0) { + $this->output .= chr($token); + } else { + throw new Exception('Invalid token.'); + } + /*elseif ($token <= 0x1f4) { + $this->output .= "\xfe" . chr($token - 0xf5); + }*/ + } + + protected function writeJid($user, $server) + { + $this->output .= "\xfa"; // 250 + if (strlen($user) > 0) { + $this->writeString($user, true); + } else { + $this->writeToken(0); + } + $this->writeString($server); + } + + protected function writeInt8($v) + { + $ret = chr($v & 0xff); + + return $ret; + } + + protected function writeInt16($v) + { + $ret = chr(($v & 0xff00) >> 8); + $ret .= chr(($v & 0x00ff) >> 0); + + return $ret; + } + + protected function writeInt20($v) + { + $ret = chr((0xF0000 & $v) >> 16); + $ret .= chr((0xFF00 & $v) >> 8); + $ret .= chr(($v & 0xFF) >> 0); + + return $ret; + } + + private function writeInt31($v) + { + $ret = chr((0x7F000000 & $v) >> 24); + $ret .= chr((0xFF0000 & $v) >> 16); + $ret .= chr((0xFF00 & $v) >> 8); + $ret .= chr(($v & 0xFF) >> 0); + + return $ret; + } + + protected function writeInt24($v) + { + $ret = chr(($v & 0xff0000) >> 16); + $ret .= chr(($v & 0x00ff00) >> 8); + $ret .= chr(($v & 0x0000ff) >> 0); + + return $ret; + } + + protected function writeBytes($bytes, $b = false) + { + $len = strlen($bytes); + $toWrite = $bytes; + if ($len >= 0x100000) { + $this->output .= "\xfe"; + $this->output .= $this->writeInt31($len); + } elseif ($len >= 0x100) { + $this->output .= "\xfd"; + $this->output .= $this->writeInt20($len); + } else { + $r = ''; + if ($b) { + if ($len < 128) { + $r = $this->tryPackAndWriteHeader(255, $bytes); + if ($r == '') { + $r = $this->tryPackAndWriteHeader(251, $bytes); + } + } + } + if ($r == '') { + $this->output .= "\xfc"; + $this->output .= $this->writeInt8($len); + } else { + $toWrite = $r; + } + } + $this->output .= $toWrite; + } + + protected function writeString($tag, $packed = false) + { + $intVal = -1; + $subdict = false; + if (TokenMap::TryGetToken($tag, $subdict, $intVal)) { + if ($subdict) { + $this->writeToken(236); + } + $this->writeToken($intVal); + + return; + } + $index = strpos($tag, '@'); + if ($index) { + $server = substr($tag, $index + 1); + $user = substr($tag, 0, $index); + $this->writeJid($user, $server); + } else { + if ($packed) { + $this->writeBytes($tag, true); + } else { + $this->writeBytes($tag); + } + } + } + + protected function writeAttributes($attributes) + { + if ($attributes) { + foreach ($attributes as $key => $value) { + $this->writeString($key); + $this->writeString($value, true); + } + } + } + + private function packByte($v, $n2) + { + switch ($v) { + case 251: + return $this->packHex($n2); + case 255: + return $this->packNibble($n2); + default: + return -1; + } + } + + private function tryPackAndWriteHeader($v, $data) + { + $length = strlen($data); + if ($length >= 128) { + return ''; + } + $array2 = array_fill(0, floor(($length + 1) / 2), 0); + for ($i = 0; $i < $length; $i++) { + $packByte = $this->packByte($v, ord($data[$i])); + if ($packByte == -1) { + $array2 = []; + break; + } + $n2 = floor($i / 2); + $array2[$n2] |= ($packByte << 4 * (1 - $i % 2)); + } + if (count($array2) > 0) { + if ($length % 2 == 1) { + $array2[count($array2) - 1] |= 0xF; + } + $string = implode(array_map('chr', $array2)); + $this->output .= chr($v); + $this->output .= $this->writeInt8($length % 2 << 7 | strlen($string)); + + return $string; + } + + return ''; + } + + protected function writeListStart($len) + { + if ($len == 0) { + $this->output .= "\x00"; + } elseif ($len < 256) { + $this->output .= "\xf8".chr($len); + } else { + $this->output .= "\xf9".$this->writeInt16($len); + } + } +} diff --git a/src/Constants.php b/src/Constants.php new file mode 100644 index 00000000..ddda22a1 --- /dev/null +++ b/src/Constants.php @@ -0,0 +1,29 @@ +logfile = $logfile; + } + + public function log($level, $message, $context = []) + { + $logline = '['.date('Y-m-d H:i:s').'] '.'['.strtoupper($level).']: '.$this->interpolate($message, $context)."\n"; + file_put_contents($this->logfile, $logline, FILE_APPEND | LOCK_EX); + } + + /** + * Interpolates context values into the message placeholders. + * + * This function is just copied from the example in the PSR-3 spec + */ + protected function interpolate($message, $context = []) + { + $replace = []; + foreach ($context as $key => $val) { + $replace['{'.$key.'}'] = $val; + } + + return strtr($message, $replace); + } +} diff --git a/src/Login.php b/src/Login.php new file mode 100644 index 00000000..7ae8d2b9 --- /dev/null +++ b/src/Login.php @@ -0,0 +1,152 @@ +parent = $parent; + $this->password = $password; + $this->phoneNumber = $this->parent->getMyNumber(); + } + + /** + * Send the nodes to the WhatsApp server to log in. + * + * @throws Exception + */ + public function doLogin() + { + if ($this->parent->isLoggedIn()) { + return true; + } + + $this->parent->writer->resetKey(); + $this->parent->reader->resetKey(); + $resource = Constants::PLATFORM.'-'.Constants::WHATSAPP_VER; + $data = $this->parent->writer->StartStream(Constants::WHATSAPP_SERVER, $resource); + $feat = $this->createFeaturesNode(); + $auth = $this->createAuthNode(); + $this->parent->sendData($data); + $this->parent->sendNode($feat); + $this->parent->sendNode($auth); + + $this->parent->pollMessage(); + $this->parent->pollMessage(); + $this->parent->pollMessage(); + + if ($this->parent->getChallengeData() != null) { + $data = $this->createAuthResponseNode(); + $this->parent->sendNode($data); + $this->parent->reader->setKey($this->inputKey); + $this->parent->writer->setKey($this->outputKey); + while (!$this->parent->pollMessage()) { + }; + } + + if ($this->parent->getLoginStatus() === Constants::DISCONNECTED_STATUS) { + throw new LoginFailureException(); + } + + $this->parent->logFile('info', '{number} successfully logged in', ['number' => $this->phoneNumber]); + $this->parent->sendAvailableForChat(); + $this->parent->sendGetPrivacyBlockedList(); + $this->parent->sendGetClientConfig(); + $this->parent->setMessageId(substr(bin2hex(mcrypt_create_iv(64, MCRYPT_DEV_URANDOM)), 0, 22)); // 11 char hex + + if (extension_loaded('curve25519') || extension_loaded('protobuf')) { + if (file_exists($this->parent->dataFolder.'axolotl-'.$this->phoneNumber.'.db')) { + $pre_keys = $this->parent->getAxolotlStore()->loadPreKeys(); + if (empty($pre_keys)) { + $this->parent->sendSetPreKeys(); + $this->parent->logFile('info', 'Sending prekeys to WA server'); + } + } + } + + return true; + } + + /** + * Add stream features. + * + * @return ProtocolNode Return itself. + */ + protected function createFeaturesNode() + { + /* $readreceipts = new ProtocolNode("readreceipts", null, null, null); + $groupsv2 = new ProtocolNode("groups_v2", null, null, null); + $privacy = new ProtocolNode("privacy", null, null, null); + $presencev2 = new ProtocolNode("presence", null, null, null);*/ + $parent = new ProtocolNode('stream:features', null, null, null); + + return $parent; + } + + /** + * Add the authentication nodes. + * + * @return ProtocolNode Returns an authentication node. + */ + protected function createAuthNode() + { + $data = $this->createAuthBlob(); + $attributes = [ + 'user' => $this->phoneNumber, + 'mechanism' => 'WAUTH-2', + + ]; + $node = new ProtocolNode('auth', $attributes, null, $data); + + return $node; + } + + protected function createAuthBlob() + { + if ($this->parent->getChallengeData()) { + $key = wa_pbkdf2('sha1', base64_decode($this->password), $this->parent->getChallengeData(), 16, 20, true); + $this->inputKey = new KeyStream($key[2], $key[3]); + $this->outputKey = new KeyStream($key[0], $key[1]); + $this->parent->reader->setKey($this->inputKey); + //$this->writer->setKey($this->outputKey); + $array = "\0\0\0\0".$this->phoneNumber.$this->parent->getChallengeData().time(); + $this->parent->setChallengeData(null); + + return $this->outputKey->EncodeMessage($array, 0, strlen($array), false); + } + } + + /** + * Add the auth response to protocoltreenode. + * + * @return ProtocolNode Returns a response node. + */ + protected function createAuthResponseNode() + { + return new ProtocolNode('response', null, null, $this->authenticate()); + } + + /** + * Authenticate with the WhatsApp Server. + * + * @return string Returns binary string + */ + protected function authenticate() + { + $keys = KeyStream::GenerateKeys(base64_decode($this->password), $this->parent->getChallengeData()); + $this->inputKey = new KeyStream($keys[2], $keys[3]); + $this->outputKey = new KeyStream($keys[0], $keys[1]); + $array = "\0\0\0\0".$this->phoneNumber.$this->parent->getChallengeData().''.time().'000'.hex2bin('00').'000'.hex2bin('00') + .Constants::OS_VERSION.hex2bin('00').Constants::MANUFACTURER.hex2bin('00').Constants::DEVICE.hex2bin('00').Constants::BUILD_VERSION; + $response = $this->outputKey->EncodeMessage($array, 0, 4, strlen($array) - 4); + $this->parent->setOutputKey($this->outputKey); + + return $response; + } +} diff --git a/src/NewMsgBindInterface.php b/src/NewMsgBindInterface.php new file mode 100644 index 00000000..3e0c44a5 --- /dev/null +++ b/src/NewMsgBindInterface.php @@ -0,0 +1,6 @@ +debug = $debug; + $this->phoneNumber = $number; + $this->eventManager = new WhatsApiEventsManager(); + $this->identity = $this->buildIdentity($customPath); // directory where identity is going to be saved + } + + /** + * Check if account credentials are valid. + * + * NOTE: WhatsApp changes your password everytime you use this. + * Make sure you update your config file if the output informs about + * a password change. + * + * @throws Exception + * + * @return object + * An object with server response. + * - status: Account status. + * - login: Phone number with country code. + * - pw: Account password. + * - type: Type of account. + * - expiration: Expiration date in UNIX TimeStamp. + * - kind: Kind of account. + * - price: Formatted price of account. + * - cost: Decimal amount of account. + * - currency: Currency price of account. + * - price_expiration: Price expiration in UNIX TimeStamp. + */ + public function checkCredentials() + { + if (!$phone = $this->dissectPhone()) { + throw new Exception('The provided phone number is not valid.'); + } + + $countryCode = ($phone['ISO3166'] != '') ? $phone['ISO3166'] : 'US'; + $langCode = ($phone['ISO639'] != '') ? $phone['ISO639'] : 'en'; + + // Build the url. + $host = 'https://'.Constants::WHATSAPP_CHECK_HOST; + $query = [ + 'cc' => $phone['cc'], + 'in' => $phone['phone'], + 'lg' => $langCode, + 'lc' => $countryCode, + 'id' => $this->identity, + 'mistyped' => '6', + 'network_radio_type' => '1', + 'simnum' => '1', + 's' => '', + 'copiedrc' => '1', + 'hasinrc' => '1', + 'rcmatch' => '1', + 'pid' => mt_rand(100, 9999), + //'rchash' => hash('sha256', openssl_random_pseudo_bytes(20)), + //'anhash' => md5(openssl_random_pseudo_bytes(20)), + 'extexist' => '1', + 'extstate' => '1', + ]; + + $this->debugPrint($query); + + $response = $this->getResponse($host, $query); + + $this->debugPrint($response); + + if ($response->status != 'ok') { + $this->eventManager()->fire('onCredentialsBad', + [ + $this->phoneNumber, + $response->status, + $response->reason, + ]); + + throw new Exception('There was a problem trying to request the code.'); + } else { + $this->eventManager()->fire('onCredentialsGood', + [ + $this->phoneNumber, + $response->login, + $response->pw, + $response->type, + $response->expiration, + $response->kind, + $response->price, + $response->cost, + $response->currency, + $response->price_expiration, + ]); + } + + return $response; + } + + /** + * Register account on WhatsApp using the provided code. + * + * @param int $code + * Numeric code value provided on requestCode(). + * + * @throws Exception + * + * @return object + * An object with server response. + * - status: Account status. + * - login: Phone number with country code. + * - pw: Account password. + * - type: Type of account. + * - expiration: Expiration date in UNIX TimeStamp. + * - kind: Kind of account. + * - price: Formatted price of account. + * - cost: Decimal amount of account. + * - currency: Currency price of account. + * - price_expiration: Price expiration in UNIX TimeStamp. + */ + public function codeRegister($code) + { + if (!$phone = $this->dissectPhone()) { + throw new Exception('The provided phone number is not valid.'); + } + + $code = str_replace('-', '', $code); + $countryCode = ($phone['ISO3166'] != '') ? $phone['ISO3166'] : 'US'; + $langCode = ($phone['ISO639'] != '') ? $phone['ISO639'] : 'en'; + + // Build the url. + $host = 'https://'.Constants::WHATSAPP_REGISTER_HOST; + $query = [ + 'cc' => $phone['cc'], + 'in' => $phone['phone'], + 'lg' => $langCode, + 'lc' => $countryCode, + 'id' => $this->identity, + 'mistyped' => '6', + 'network_radio_type' => '1', + 'simnum' => '1', + 's' => '', + 'copiedrc' => '1', + 'hasinrc' => '1', + 'rcmatch' => '1', + 'pid' => mt_rand(100, 9999), + 'rchash' => hash('sha256', openssl_random_pseudo_bytes(20)), + 'anhash' => md5(openssl_random_pseudo_bytes(20)), + 'extexist' => '1', + 'extstate' => '1', + 'code' => $code, + ]; + + $this->debugPrint($query); + + $response = $this->getResponse($host, $query); + + $this->debugPrint($response); + + if ($response->status != 'ok') { + $this->eventManager()->fire('onCodeRegisterFailed', + [ + $this->phoneNumber, + $response->status, + $response->reason, + isset($response->retry_after) ? $response->retry_after : null, + ]); + + if ($response->reason == 'old_version') { + $this->update(); + } + + throw new Exception("An error occurred registering the registration code from WhatsApp. Reason: $response->reason"); + } else { + $this->eventManager()->fire('onCodeRegister', + [ + $this->phoneNumber, + $response->login, + $response->pw, + $response->type, + $response->expiration, + $response->kind, + $response->price, + $response->cost, + $response->currency, + $response->price_expiration, + ]); + } + + return $response; + } + + /** + * Request a registration code from WhatsApp. + * + * @param string $method Accepts only 'sms' or 'voice' as a value. + * @param string $carrier + * + * @throws Exception + * + * @return object + * An object with server response. + * - status: Status of the request (sent/fail). + * - length: Registration code lenght. + * - method: Used method. + * - reason: Reason of the status (e.g. too_recent/missing_param/bad_param). + * - param: The missing_param/bad_param. + * - retry_after: Waiting time before requesting a new code. + */ + public function codeRequest($method = 'sms', $carrier = 'T-Mobile5', $platform = 'Android') + { + if (!$phone = $this->dissectPhone()) { + throw new Exception('The provided phone number is not valid.'); + } + + $countryCode = ($phone['ISO3166'] != '') ? $phone['ISO3166'] : 'US'; + $langCode = ($phone['ISO639'] != '') ? $phone['ISO639'] : 'en'; + + if ($carrier != null) { + $mnc = $this->detectMnc(strtolower($countryCode), $carrier); + } else { + $mnc = $phone['mnc']; + } + + // Build the token. + $token = generateRequestToken($phone['country'], $phone['phone'], $platform); + + // Build the url. + $host = 'https://'.Constants::WHATSAPP_REQUEST_HOST; + $query = [ + 'cc' => $phone['cc'], + 'in' => $phone['phone'], + 'lg' => $langCode, + 'lc' => $countryCode, + 'id' => $this->identity, + 'token' => $token, + 'mistyped' => '6', + 'network_radio_type' => '1', + 'simnum' => '1', + 's' => '', + 'copiedrc' => '1', + 'hasinrc' => '1', + 'rcmatch' => '1', + 'pid' => mt_rand(100, 9999), + 'rchash' => hash('sha256', openssl_random_pseudo_bytes(20)), + 'anhash' => md5(openssl_random_pseudo_bytes(20)), + 'extexist' => '1', + 'extstate' => '1', + 'mcc' => $phone['mcc'], + 'mnc' => $mnc, + 'sim_mcc' => $phone['mcc'], + 'sim_mnc' => $mnc, + 'method' => $method, + //'reason' => "self-send-jailbroken", + ]; + + $this->debugPrint($query); + + $response = $this->getResponse($host, $query); + + $this->debugPrint($response); + + if ($response->status == 'ok') { + $this->eventManager()->fire('onCodeRegister', + [ + $this->phoneNumber, + $response->login, + $response->pw, + $response->type, + $response->expiration, + $response->kind, + $response->price, + $response->cost, + $response->currency, + $response->price_expiration, + ]); + } elseif ($response->status != 'sent') { + if (isset($response->reason) && $response->reason == 'too_recent') { + $this->eventManager()->fire('onCodeRequestFailedTooRecent', + [ + $this->phoneNumber, + $method, + $response->reason, + $response->retry_after, + ]); + $minutes = round($response->retry_after / 60); + throw new Exception("Code already sent. Retry after $minutes minutes."); + } elseif (isset($response->reason) && $response->reason == 'too_many_guesses') { + $this->eventManager()->fire('onCodeRequestFailedTooManyGuesses', + [ + $this->phoneNumber, + $method, + $response->reason, + $response->retry_after, + ]); + $minutes = round($response->retry_after / 60); + throw new Exception("Too many guesses. Retry after $minutes minutes."); + } else { + $this->eventManager()->fire('onCodeRequestFailed', + [ + $this->phoneNumber, + $method, + $response->reason, + isset($response->param) ? $response->param : null, + ]); + throw new Exception('There was a problem trying to request the code.'); + } + } else { + $this->eventManager()->fire('onCodeRequest', + [ + $this->phoneNumber, + $method, + $response->length, + ]); + } + + return $response; + } + + /** + * Get a decoded JSON response from Whatsapp server. + * + * @param string $host The host URL + * @param array $query A associative array of keys and values to send to server. + * + * @return null|object NULL if the json cannot be decoded or if the encoded data is deeper than the recursion limit + */ + protected function getResponse($host, $query) + { + // Build the url. + $url = $host.'?'.http_build_query($query); + + // Open connection. + $ch = curl_init(); + + // Configure the connection. + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_USERAGENT, Constants::WHATSAPP_USER_AGENT); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: text/json']); + // This makes CURL accept any peer! + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + // Get the response. + $response = curl_exec($ch); + + // Close the connection. + curl_close($ch); + + return json_decode($response); + } + + /** + * Dissect country code from phone number. + * + * @return array + * An associative array with country code and phone number. + * - country: The detected country name. + * - cc: The detected country code (phone prefix). + * - phone: The phone number. + * - ISO3166: 2-Letter country code + * - ISO639: 2-Letter language code + * Return false if country code is not found. + */ + protected function dissectPhone() + { + if (($handle = fopen(dirname(__FILE__).'/countries.csv', 'rb')) !== false) { + while (($data = fgetcsv($handle, 1000)) !== false) { + if (strpos($this->phoneNumber, $data[1]) === 0) { + // Return the first appearance. + fclose($handle); + + $mcc = explode('|', $data[2]); + $mcc = $mcc[0]; + + //hook: + //fix country code for North America + if ($data[1][0] == '1') { + $data[1] = '1'; + } + + $phone = [ + 'country' => $data[0], + 'cc' => $data[1], + 'phone' => substr($this->phoneNumber, strlen($data[1]), strlen($this->phoneNumber)), + 'mcc' => $mcc, + 'ISO3166' => @$data[3], + 'ISO639' => @$data[4], + 'mnc' => $data[5], + ]; + + $this->eventManager()->fire('onDissectPhone', + [ + $this->phoneNumber, + $phone['country'], + $phone['cc'], + $phone['phone'], + $phone['mcc'], + $phone['ISO3166'], + $phone['ISO639'], + $phone['mnc'], + ] + ); + + return $phone; + } + } + fclose($handle); + } + + $this->eventManager()->fire('onDissectPhoneFailed', + [ + $this->phoneNumber, + ]); + + return false; + } + + /** + * Detects mnc from specified carrier. + * + * @param string $lc LangCode + * @param string $carrierName Name of the carrier + * + * @return string + * + * Returns mnc value + */ + protected function detectMnc($lc, $carrierName) + { + $fp = fopen(__DIR__.DIRECTORY_SEPARATOR.'networkinfo.csv', 'r'); + $mnc = null; + + while ($data = fgetcsv($fp, 0, ',')) { + if ($data[4] === $lc && $data[7] === $carrierName) { + $mnc = $data[2]; + break; + } + } + + if ($mnc == null) { + $mnc = '000'; + } + + fclose($fp); + + return $mnc; + } + + public function update() + { + $WAData = json_decode(file_get_contents(Constants::WHATSAPP_VER_CHECKER), true); + $WAver = $WAData['e']; + + if (Constants::WHATSAPP_VER != $WAver) { + updateData('token.php', null, $WAData['h']); + updateData('Constants.php', $WAver); + } + } + + /** + * Create an identity string. + * + * @param mixed $identity_file IdentityFile (optional). + * + * @throws Exception Error when cannot write identity data to file. + * + * @return string Correctly formatted identity + */ + protected function buildIdentity($identity_file = false) + { + if ($identity_file === false) { + $identity_file = sprintf('%s%s%sid.%s.dat', __DIR__, DIRECTORY_SEPARATOR, Constants::DATA_FOLDER.DIRECTORY_SEPARATOR, $this->phoneNumber); + } + + // Check if the provided is not a file but a directory + if (is_dir($identity_file)) { + $identity_file = sprintf('%s/id.%s.dat', + rtrim($identity_file, "/"), + $this->phoneNumber + ); + } + + if (is_readable($identity_file)) { + $data = urldecode(file_get_contents($identity_file)); + $length = strlen($data); + + if ($length == 20 || $length == 16) { + return $data; + } + } + + $bytes = strtolower(openssl_random_pseudo_bytes(20)); + + if (file_put_contents($identity_file, urlencode($bytes)) === false) { + throw new Exception('Unable to write identity file to '.$identity_file); + } + + return $bytes; + } + + /** + * Print a message to the debug console. + * + * @param mixed $debugMsg The debug message. + * + * @return bool + */ + protected function debugPrint($debugMsg) + { + if ($this->debug) { + if (is_array($debugMsg) || is_object($debugMsg)) { + print_r($debugMsg); + } else { + echo $debugMsg; + } + + return true; + } + + return false; + } + + /** + * @return WhatsApiEventsManager + */ + public function eventManager() + { + return $this->eventManager; + } +} diff --git a/src/SqliteAxolotlStore.php b/src/SqliteAxolotlStore.php new file mode 100644 index 00000000..18217514 --- /dev/null +++ b/src/SqliteAxolotlStore.php @@ -0,0 +1,601 @@ +filename = $customPath.'axolotl-'.$number.'.db'; + } else { + $this->filename = __DIR__.DIRECTORY_SEPARATOR.self::DATA_FOLDER.DIRECTORY_SEPARATOR.'axolotl-'.$number.'.db'; + } + + $this->create(); + } + + protected function create() + { + $createTable = !file_exists($this->filename); + + $this->db = new \PDO('sqlite:'.$this->filename, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + if ($createTable) { + //create necesary tables before starting + $this->db->exec('CREATE TABLE IF NOT EXISTS identities + ( + `_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `recipient_id` INTEGER UNIQUE, + `registration_id` INTEGER, + `public_key` BLOB, + `private_key` BLOB, + `next_prekey_id` INTEGER, + `timestamp` INTEGER + );'); + $this->db->exec('CREATE TABLE IF NOT EXISTS prekeys + ( + `_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `prekey_id` INTEGER UNIQUE, + `sent_to_server` BOOLEAN, + `record` BLOB + );'); + //$this->db->exec('CREATE TABLE IF NOT EXISTS sender_keys + // (`group_id` TEXT, sender_id TEXT, record TEXT)'); + $this->db->exec('CREATE TABLE IF NOT EXISTS sessions + ( + `_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `recipient_id` INTEGER UNIQUE, + `device_id` INTEGER, + `record` BLOB, + `timestamp` INTEGER + );'); + $this->db->exec('CREATE TABLE IF NOT EXISTS signed_prekeys + ( + `_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `prekey_id` INTEGER UNIQUE, + `timestamp` INTEGER, + `record` BLOB + );'); + $this->db->exec('CREATE TABLE IF NOT EXISTS sender_keys + ( + `_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `sender_key_id` TEXT UNIQUE, + `record` BLOB + );'); + } + } + + //prekeys + + public function storePreKey($prekeyId, $record) + { + $sql = 'INSERT INTO prekeys (`prekey_id`, `record`) VALUES (:prekey_id, :record)'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':prekey_id' => $prekeyId, + ':record' => $record->serialize(), + ] + ); + } + + public function storePreKeys($keys) + { + $this->db->beginTransaction(); + foreach ($keys as $key) { + $this->storePreKey($key->getId(), $key); + } + $this->db->commit(); + } + + public function loadPreKey($preKeyId) + { + $sql = 'SELECT `record` FROM prekeys where `prekey_id` = :id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':id' => $preKeyId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + if ($row == null || $row === false) { + throw new Exception('No such prekey with id: '.$preKeyId); + } + + return new PreKeyRecord(null, null, $row['record']); + } + + public function loadPreKeys() + { + $sql = 'SELECT `record` FROM prekeys'; + $query = $this->db->prepare($sql); + + $query->execute(); + $prekeys = []; + while ($row = $query->fetch(PDO::FETCH_ASSOC)) { + if ($row != null && $row !== false) { + $prekeys[] = $row['record']; + } + } + + return $prekeys; + } + + public function containsPreKey($preKeyId) + { + $sql = 'SELECT `record` FROM prekeys where `prekey_id` = :id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':id' => $preKeyId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + if ($row == null || $row === false) { + return false; + } + + return true; + } + + public function removePreKey($preKeyId) + { + $sql = 'DELETE FROM prekeys WHERE `prekey_id` = :id'; + $query = $this->db->prepare($sql); + $query->execute( + [ + ':id' => $preKeyId, + ] + ); + } + + public function removeAllPreKeys() + { + $sql = 'DELETE FROM prekeys'; + $query = $this->db->prepare($sql); + $query->execute(); + } + + //signedPreKey + + public function loadSignedPreKey($signedPreKeyId) + { + $sql = 'SELECT `record` FROM signed_prekeys where `prekey_id` = :id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':id' => $signedPreKeyId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + if ($row == null || $row === false) { + throw new Exception('No such signedprekey with id: '.$signedPreKeyId); + } + + return new SignedPreKeyRecord(null, null, null, null, $row['record']); + } + + public function loadSignedPreKeys() + { + $sql = 'SELECT `record` FROM signed_prekeys'; + $query = $this->db->prepare($sql); + + $query->execute(); + $keys = []; + while ($row = $query->fetch(PDO::FETCH_ASSOC)) { + if ($row != null && $row !== false) { + $keys[] = new SignedPreKeyRecord(null, null, null, null, $row['record']); + } + } + + return $keys; + } + + public function storeSignedPreKey($signedPreKeyId, $signedPreKeyRecord) + { + $sql = 'INSERT INTO signed_prekeys (`prekey_id`, `record`) VALUES (:prekey_id, :record)'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':prekey_id' => $signedPreKeyId, + ':record' => $signedPreKeyRecord->serialize(), + ] + ); + } + + public function removeSignedPreKey($signedPreKeyId) + { + $sql = 'DELETE FROM signed_prekeys WHERE `prekey_id` = :id'; + $query = $this->db->prepare($sql); + $query->execute( + [ + ':id' => $signedPreKeyId, + ] + ); + } + + public function containsSignedPreKey($signedPreKeyId) + { + $sql = 'SELECT `record` FROM signed_prekeys where `prekey_id` = :id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':id' => $signedPreKeyId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + if ($row == null || $row === false) { + return false; + } + + return true; + } + + //identity + + public function getIdentityKeyPair() + { + $sql = 'SELECT `public_key`, `private_key` FROM identities where recipient_id = -1'; + $query = $this->db->prepare($sql); + + $query->execute(); + $row = $query->fetch(PDO::FETCH_ASSOC); + + if ($row != null && $row !== false) { + $keys = new IdentityKeyPair( + new IdentityKey(new DjbECPublicKey(substr($row['public_key'], 1))), + new DjbECPrivateKey($row['private_key']) + ); + } else { + + //this should not happen + $keys = null; + } + + return $keys; + } + + public function getLocalRegistrationId() + { + $sql = 'SELECT `registration_id` FROM identities WHERE recipient_id = -1'; + $query = $this->db->prepare($sql); + + $query->execute(); + $row = $query->fetch(PDO::FETCH_ASSOC); + + if ($row != null && $row !== false) { + $localRegistrationId = $row['registration_id']; + } else { + $localRegistrationId = null; + } + + return $localRegistrationId; + } + + public function storeLocalData($registrationId, $identityKeyPair) + { + $sql = 'INSERT OR REPLACE INTO identities(recipient_id, registration_id, public_key, private_key) + VALUES (:recipient_id, :registration_id, :public_key, :private_key)'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => -1, + ':registration_id' => $registrationId, + ':public_key' => $identityKeyPair->getPublicKey()->serialize(), //this should be tested, identityKeyPair.getPublicKey().getPublicKey().serialize() + ':private_key' => $identityKeyPair->getPrivateKey()->serialize(), + ] + ); + } + + public function clearRecipient($recipientId) + { + $sql = 'DELETE FROM identities where recipient_id = :recipient_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ] + ); + $sql = 'DELETE FROM sessions where recipient_id = :recipient_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ] + ); + } + + public function isTrustedIdentity($recipientId, $identityKey) + { + /* + $sql = 'SELECT public_key from identities WHERE recipient_id = :recipient_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + if ($row == null || $row === false) { + return true; + } + + return $row['public_key'] == $identityKey->getPublicKey()->serialize(); + */ + return true; + } + + public function saveIdentity($recipientId, $identityKey) + { + $sql = 'DELETE FROM identities WHERE recipient_id = :recipient_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ] + ); + + $sql = 'INSERT INTO identities (recipient_id, public_key) VALUES(:recipient_id, :public_key)'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ':public_key' => $identityKey->getPublicKey()->serialize(), + ] + ); + } + + //session + + public function loadSession($recipientId, $deviceId) + { + $sql = 'SELECT record FROM sessions WHERE recipient_id = :recipient_id AND device_id = :device_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ':device_id' => $deviceId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + + if ($row != null && $row !== false) { + $SessionRecord = new SessionRecord(null, $row['record']); + } else { + $SessionRecord = new SessionRecord(); + } + + return $SessionRecord; + } + + public function getSubDeviceSessions($recipientId) + { + $sql = 'SELECT device_id from sessions WHERE recipient_id = :recipient_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ] + ); + $deviceIds = []; + while ($row = $query->fetch(PDO::FETCH_ASSOC)) { + $deviceIds[] = $row['device_id']; + } + + return $deviceIds; + } + + public function storeSession($recipientId, $deviceId, $sessionRecord) + { + $this->deleteSession($recipientId, 1); + $sql = 'INSERT INTO sessions(recipient_id, device_id, record) VALUES (:recipient_id, :device_id, :record)'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ':device_id' => $deviceId, + ':record' => $sessionRecord->serialize(), + ] + ); + } + + public function containsSession($recipientId, $deviceId) + { + $sql = 'SELECT record FROM sessions WHERE recipient_id = :recipient_id AND device_id = :device_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':recipient_id' => $recipientId, + ':device_id' => $deviceId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + if ($row == null || $row === false) { + return false; + } + + return true; + } + + public function deleteSession($recipientId, $deviceId) + { + $sql = 'DELETE FROM sessions WHERE recipient_id = :recipient_id AND device_id = :device_id'; + $query = $this->db->prepare($sql); + $query->bindParam(':recipient_id', $recipientId, PDO::PARAM_INT); + $query->bindParam(':device_id', $deviceId, PDO::PARAM_INT); + $query->execute(); + } + + public function deleteAllSessions($recipientId) + { + $sql = 'DELETE FROM sessions WHERE recipient_id = :recipient_id'; + $query = $this->db->prepare($sql); + $query->execute( + [ + ':recipient_id' => $recipientId, + ] + ); + } + + //sender_keys + + public function storeSenderKey($senderKeyId, $senderKeyRecord) + { + $this->removeSenderKey($senderKeyId); + $sql = 'INSERT INTO sender_keys(sender_key_id, record) VALUES (:sender_key_id, :record)'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':sender_key_id' => $senderKeyId, + ':record' => $senderKeyRecord->serialize(), + ] + ); + } + + public function removeSenderKey($senderKeyId) + { + $sql = 'DELETE FROM sender_keys where sender_key_id = :sender_key_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':sender_key_id' => $senderKeyId, + ] + ); + } + + public function loadSenderKey($senderKeyId) + { + $sql = 'SELECT record FROM sender_keys WHERE sender_key_id = :sender_key_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':sender_key_id' => $senderKeyId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + $record = new SenderKeyRecord(); + if ($row != null && $row !== false) { + $record = new SenderKeyRecord($row['record']); + } + + return $record; + } + + public function containsSenderKey($senderKeyId) + { + $sql = 'SELECT record FROM sender_keys WHERE sender_key_id = :sender_key_id'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':sender_key_id' => $senderKeyId, + ] + ); + $row = $query->fetch(PDO::FETCH_ASSOC); + + if ($row === null && $row === false) { + return false; + } + + return true; + } + + public function clear() + { + if (file_exists($this->filename)) { + unlink($this->filename); + } + $this->create(); + } +} diff --git a/src/SqliteMessageStore.php b/src/SqliteMessageStore.php new file mode 100644 index 00000000..3012bfce --- /dev/null +++ b/src/SqliteMessageStore.php @@ -0,0 +1,106 @@ +db = new \PDO('sqlite:'.$fileName, null, null, [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + if ($createTable) { + $this->db->exec('CREATE TABLE messages (`from` TEXT, `to` TEXT, message TEXT, id TEXT, t TEXT)'); + $this->db->exec('CREATE TABLE messages_pending(`id` TEXT PRIMARY KEY,`jid` TEXT, `pending` TINYINT(1) DEFAULT 0)'); + } else { + //backward compatibility + $result = $this->db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_pending';")->fetchAll(); + if ($result == null || $result == false || count($result) == 0) { + $this->db->exec('CREATE TABLE messages_pending(`id` TEXT PRIMARY KEY,`jid` TEXT, `pending` TINYINT(1) DEFAULT 0)'); + } + } + } + + public function saveMessage($from, $to, $txt, $id, $t) + { + $sql = 'INSERT INTO messages (`from`, `to`, message, id, t) VALUES (:from, :to, :message, :messageId, :t)'; + $query = $this->db->prepare($sql); + + $query->execute( + [ + ':from' => $from, + ':to' => $to, + ':message' => $txt, + ':messageId' => $id, + ':t' => $t, + ] + ); + } + + public function setPending($id, $jid) + { + $sql = 'UPDATE messages_pending set `pending` = 1, `jid` = :jid where `id` = :id'; + $query = $this->db->prepare($sql); + $query->execute( + [ + ':id' => $id, + ':jid' => $jid, + ] + ); + $sql = 'INSERT OR IGNORE into messages_pending(`id`,`jid`, `pending`) VALUES(:id,:jid,1)'; + $query = $this->db->prepare($sql); + $query->execute( + [ + ':id' => $id, + ':jid' => $jid, + ] + ); + } + + public function getPending($jid) + { + $sql = 'SELECT `id` from messages_pending where `jid` = :jid and `pending` = 1'; + $query = $this->db->prepare($sql); + $query->execute( + [ + ':jid' => $jid, + ] + ); + $pending_ids = []; + while ($row = $query->fetch(PDO::FETCH_ASSOC)) { + if ($row != null && $row !== false) { + $pending_ids[] = $row['id']; + } + } + if (count($pending_ids) == 0) { + return []; + } + $messages = []; + $qMarks = str_repeat('?,', count($pending_ids) - 1).'?'; + $sql = "SELECT * from messages where `id` IN ($qMarks)"; + $query = $this->db->prepare($sql); + $query->execute($pending_ids); + while ($row = $query->fetch(PDO::FETCH_ASSOC)) { + if ($row != null && $row !== false) { + $messages[] = $row; + } + } + $sql = 'DELETE FROM messages_pending where `pending` = 1 and jid = :jid'; + $query = $this->db->prepare($sql); + $query->execute([':jid' => $jid]); + + return $messages; + } +} diff --git a/src/WhatsAppEvent.php b/src/WhatsAppEvent.php deleted file mode 100755 index b291ca6e..00000000 --- a/src/WhatsAppEvent.php +++ /dev/null @@ -1,804 +0,0 @@ -addEventListener(new WhatsAppEventListenerLegacyAdapter($event,$listener)); - } - - /** - * Executes all the binded callbacks when the event is fired. Don't this method, - * this is included for backwards compatibility only. - * - * @param string $event - * Name of the event. - * @param array $arguments - * The arguments to pass to each callback. - * - * @deprecated Fire events specifically by name. - */ - public function fire($event, $arguments = array()) - { - // For backwards compatibility only. - foreach( self::$event_listeners as $event_listener ) { - call_user_func_array(array($event_listener, $event), $arguments); - } - } - - /** - * Fires the callback for each listener. - * - * @param function $callbackEvent - */ - private function fireCallback($callbackEvent) { - array_map($callbackEvent, self::$event_listeners); - } - - // The supported events: - function fireClose( - $phone, - $error - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $error) { - $listener->onClose($phone, $error); - }; - $this->fireCallback($callbackEvent); - } - - function fireCodeRegister( - $phone, - $login, - $pw, - $type, - $expiration, - $kind, - $price, - $cost, - $currency, - $price_expiration - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $login, $pw, $type, $expiration, $kind, $price, $cost, $currency, $price_expiration) { - $listener->onCodeRegister($phone, $login, $pw, $type, $expiration, $kind, $price, $cost, $currency, $price_expiration); - }; - $this->fireCallback($callbackEvent); - } - - function fireCodeRegisterFailed( - $phone, - $status, - $reason, - $retry_after - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $status, $reason, $retry_after) { - $listener->onCodeRegisterFailed($phone, $status, $reason, $retry_after); - }; - $this->fireCallback($callbackEvent); - } - - function fireCodeRequest( - $phone, - $method, - $length - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $method, $length) { - $listener->onCodeRequest($phone, $method, $length); - }; - $this->fireCallback($callbackEvent); - } - - function fireCodeRequestFailed( - $phone, - $method, - $reason, - $value - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $method, $reason, $value) { - $listener->onCodeRequestFailed($phone, $method, $reason, $value); - }; - $this->fireCallback($callbackEvent); - } - - function fireCodeRequestFailedTooRecent( - $phone, - $method, - $reason, - $retry_after - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $method, $reason, $retry_after){ - $listener->onCodeRequestFailedTooRecent($phone, $method, $reason, $retry_after); - }; - $this->fireCallback($callbackEvent); - } - - function fireConnect( - $phone, - $socket - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $socket) { - $listener->onConnect($phone, $socket); - }; - $this->fireCallback($callbackEvent); - } - - function fireConnectError( - $phone, - $socket - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $socket){ - $listener->onConnectError($phone, $socket); - }; - $this->fireCallback($callbackEvent); - } - - function fireCredentialsBad( - $phone, - $status, - $reason - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $status, $reason) { - $listener->onCredentialsBad($phone, $status, $reason); - }; - $this->fireCallback($callbackEvent); - } - - function fireCredentialsGood( - $phone, - $login, - $pw, - $type, - $expiration, - $kind, - $price, - $cost, - $currency, - $price_expiration - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $login, $pw, $type, $expiration, $kind, $price, $cost, $currency, $price_expiration) { - $listener->onCredentialsGood($phone, $login, $pw, $type, $expiration, $kind, $price, $cost, $currency, $price_expiration); - }; - $this->fireCallback($callbackEvent); - } - - function fireDisconnect( - $phone, - $socket - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $socket) { - $listener->onDisconnect($phone, $socket); - }; - $this->fireCallback($callbackEvent); - } - - function fireDissectPhone( - $phone, - $country, - $cc, - $mcc, - $lc, - $lg - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $country, $cc, $mcc, $lc, $lg) { - $listener->onDissectPhone($phone, $country, $cc, $mcc, $lc, $lg); - }; - $this->fireCallback($callbackEvent); - } - - function fireDissectPhoneFailed( - $phone - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone) { - $listener->onDissectPhoneFailed($phone); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetAudio( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $size, - $url, - $file, - $mimetype, - $filehash, - $duration, - $acodec - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time, $name, $size, $url, $file, $mimetype, $filehash, $duration, $acodec) { - $listener->onGetAudio($phone, $from, $msgid, $type, $time, $name, $size, $url, $file, $mimetype, $filehash, $duration, $acodec); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetError( - $phone, - $id, - $error - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $id, $error) { - $listener->onGetError($phone, $id, $error); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetGroups( - $phone, - $groupList - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $groupList) { - $listener->onGetGroups($phone, $groupList); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetGroupsInfo( - $phone, - $groupList - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $groupList) { - $listener->onGetGroupsInfo($phone, $groupList); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetGroupsSubject( - $phone, - $gId, - $time, - $author, - $participant, - $name, - $subject - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $gId, $time, $author, $participant, $name, $subject) { - $listener->onGetGroupsSubject($phone, $gId, $time, $author, $participant, $name, $subject); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetImage( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $size, - $url, - $file, - $mimetype, - $filehash, - $width, - $height, - $thumbnail - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time, $name, $size, $url, $file, $mimetype, $filehash, $width, $height, $thumbnail) { - $listener->onGetImage($phone, $from, $msgid, $type, $time, $name, $size, $url, $file, $mimetype, $filehash, $width, $height, $thumbnail); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetLocation( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $place_name, - $longitude, - $latitude, - $url, - $thumbnail - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time, $name, $place_name, $longitude, $latitude, $url, $thumbnail) { - $listener->onGetLocation($phone, $from, $msgid, $type, $time, $name, $place_name, $longitude, $latitude, $url, $thumbnail); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetMessage( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $message - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time, $name, $message) { - $listener->onGetMessage($phone, $from, $msgid, $type, $time, $name, $message); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetGroupMessage( - $phone, - $from, - $author, - $msgid, - $type, - $time, - $name, - $message - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $author, $msgid, $type, $time, $name, $message) { - $listener->onGetGroupMessage($phone, $from, $author, $msgid, $type, $time, $name, $message); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetGroupParticipants( - $phone, - $groupId, - $groupList - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $groupId, $groupList) { - $listener->onGetGroupParticipants($phone, $groupId, $groupList); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetPrivacyBlockedList( - $phone, - $children - /* - $data, - $onGetProfilePicture, - $phone, - $from, - $type, - $thumbnail - */ - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $children) { - $listener->onGetPrivacyBlockedList($phone, $children); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetProfilePicture( - $phone, - $from, - $type, - $thumbnail - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $type, $thumbnail) { - $listener->onGetProfilePicture($phone, $from, $type, $thumbnail); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetRequestLastSeen( - $phone, - $from, - $msgid, - $sec - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $sec) { - $listener->onGetRequestLastSeen($phone, $from, $msgid, $sec); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetServerProperties( - $phone, - $version, - $properties - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $version, $properties) { - $listener->onGetServerProperties($phone, $version, $properties); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetStatus( - $phone, - $from, - $type, - $id, - $t, - $status - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $type, $id, $t, $status) { - $listener->onGetStatus($phone, $from, $type, $id, $t, $status); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetvCard( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $contact, - $vcard - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time, $name, $contact, $vcard){ - $listener->onGetvCard($phone, $from, $msgid, $type, $time, $name, $contact, $vcard); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetVideo( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $url, - $file, - $size, - $mimetype, - $filehash, - $duration, - $vcodec, - $acodec, - $thumbnail - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time, $name, $url, $file, $size, $mimetype, $filehash, $duration, $vcodec, $acodec, $thumbnail){ - $listener->onGetVideo($phone, $from, $msgid, $type, $time, $name, $url, $file, $size, $mimetype, $filehash, $duration, $vcodec, $acodec, $thumbnail); - }; - $this->fireCallback($callbackEvent); - } - - function fireGroupsChatCreate( - $phone, - $gId - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $gId) { - $listener->onGroupsChatCreate($phone, $gId); - }; - $this->fireCallback($callbackEvent); - } - - function fireGroupsChatEnd( - $phone, - $gId - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $gId) { - $listener->onGroupsChatEnd($phone, $gId); - }; - $this->fireCallback($callbackEvent); - } - - function fireGroupsParticipantsAdd( - $phone, - $groupId, - $participant - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $groupId, $participant) { - $listener->onGroupsParticipantsAdd($phone, $groupId, $participant); - }; - $this->fireCallback($callbackEvent); - } - - function fireGroupsParticipantsRemove( - $phone, - $groupId, - $participant, - $author - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $groupId, $participant, $author) { - $listener->onGroupsParticipantsRemove($phone, $groupId, $participant, $author); - }; - $this->fireCallback($callbackEvent); - } - - function fireLogin( - $phone - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone) { - $listener->onLogin($phone); - }; - $this->fireCallback($callbackEvent); - } - - function fireLoginFailed( - $phone, - $tag - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $tag) { - $listener->onLoginFailed($phone, $tag); - }; - $this->fireCallback($callbackEvent); - } - - function fireMessageComposing( - $phone, - $from, - $msgid, - $type, - $time - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time) { - $listener->onMessageComposing($phone, $from, $msgid, $type, $time); - }; - $this->fireCallback($callbackEvent); - } - - function fireMediaMessageSent( - $phone, - $to, - $id, - $filetype, - $url, - $filename, - $filesize, - $filehash, - $icon - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $to, $id, $filetype, $url, $filename, $filesize, $filehash, $icon) { - $listener->onMediaMessageSent($phone, $to, $id, $filetype, $url, $filename, $filesize, $filehash, $icon); - }; - $this->fireCallback($callbackEvent); - } - - function fireMediaUploadFailed( - $phone, - $id, - $node, - $messageNode, - $reason - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $id, $node, $messageNode, $reason) { - $listener->onMediaUploadFailed($phone, $id, $node, $messageNode, $reason); - }; - $this->fireCallback($callbackEvent); - } - - function fireMessagePaused( - $phone, - $from, - $msgid, - $type, - $time - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time) { - $listener->onMessagePaused($phone, $from, $msgid, $type, $time); - }; - $this->fireCallback($callbackEvent); - } - - function fireMessageReceivedClient( - $phone, - $from, - $msgid, - $type, - $time - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type, $time) { - $listener->onMessageReceivedClient($phone, $from, $msgid, $type, $time); - }; - $this->fireCallback($callbackEvent); - } - - function fireMessageReceivedServer( - $phone, - $from, - $msgid, - $type - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $msgid, $type) { - $listener->onMessageReceivedServer($phone, $from, $msgid, $type); - }; - $this->fireCallback($callbackEvent); - } - - function firePing( - $phone, - $msgid - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $msgid) { - $listener->onPing($phone, $msgid); - }; - $this->fireCallback($callbackEvent); - } - - function firePresence( - $phone, - $from, - $type - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $type) { - $listener->onPresence($phone, $from, $type); - }; - $this->fireCallback($callbackEvent); - } - - function fireProfilePictureChanged( - $phone, - $from, - $id, - $t - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $id, $t) { - $listener->onProfilePictureChanged($phone, $from, $id, $t); - }; - $this->fireCallback($callbackEvent); - } - - function fireProfilePictureDeleted( - $phone, - $from, - $id, - $t - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $from, $id, $t) { - $listener->onProfilePictureDeleted($phone, $from, $id, $t); - }; - $this->fireCallback($callbackEvent); - } - - function fireSendMessageReceived( - $phone, - $id, - $from, - $type - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $id, $from, $type) { - $listener->onSendMessageReceived($phone, $id, $from, $type); - }; - $this->fireCallback($callbackEvent); - } - - function fireSendPong( - $phone, - $msgid - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $msgid) { - $listener->onSendPong($phone, $msgid); - }; - $this->fireCallback($callbackEvent); - } - - function fireSendMessage( - $phone, - $targets, - $id, - $node - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $targets, $id, $node) { - $listener->onSendMessage($phone, $targets, $id, $node); - }; - $this->fireCallback($callbackEvent); - } - - function fireSendPresence( - $phone, - $type, - $name - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $type, $name) { - $listener->onSendPresence($phone, $type, $name); - }; - $this->fireCallback($callbackEvent); - } - - function fireSendStatusUpdate( - $phone, - $msg - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $msg) { - $listener->onSendStatusUpdate($phone, $msg); - }; - $this->fireCallback($callbackEvent); - } - - function fireUploadFile( - $phone, - $name, - $url - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $name, $url) { - $listener->onUploadFile($phone, $name, $url); - }; - $this->fireCallback($callbackEvent); - } - - function fireUploadFileFailed( - $phone, - $name - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($phone, $name) { - $listener->onUploadFileFailed($phone, $name); - }; - $this->fireCallback($callbackEvent); - } - - /** - * @param $result SyncResult - */ - function fireGetSyncResult( - $result - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($result) { - $listener->onGetSyncResult($result); - }; - $this->fireCallback($callbackEvent); - } - - function fireGetReceipt( - $from, - $id, - $offline, - $retry - ) { - $callbackEvent = function(WhatsAppEventListener $listener) use ($from, $id, $offline, $retry) { - $listener->onGetReceipt($from, $id, $offline, $retry); - }; - $this->fireCallback($callbackEvent); - } - -} diff --git a/src/countries.csv b/src/countries.csv index a3d79597..a9e318bb 100755 --- a/src/countries.csv +++ b/src/countries.csv @@ -1,254 +1,254 @@ -"Afghanistan",93,412,"AF","ps" -"Albania",355,276,"AL","sq" -"Alberta",1403,302,"CA","en" -"Alberta",1780,302,"CA","en" -"Algeria",213,603,"DZ","ar" -"Andorra",376,213,"AD","ca" -"Angola",244,631,"AO","pt" -"Anguilla",1264,"365","AI","en" -"Antarctica (Australian bases)",6721,232,"AQ","en" -"Antigua and Barbuda",1268,"344","AG","en" -"Argentina",54,722,"AR","es" -"Armenia",374,283,"AM","hy" -"Aruba",297,363,"AW","nl" -"Ascension",247,658,"AC","en" -"Australia",61,505,"AU","en" -"Austria",43,232,"AT","de" -"Azerbaijan",994,400,"AZ","az" -"Bahamas",1242,"364","BS","en" -"Bahrain",973,426,"BH","ar" -"Bangladesh",880,470,"BD","bn" -"Barbados",1246,"342","BB","en" -"Belarus",375,257,"BY","be" -"Belgium",32,206,"BE","nl" -"Belize",501,702,"BZ","es" -"Benin",229,616,"BJ","fr" -"Bermuda",1441,"350","BM","en" -"Bhutan",975,402,"BT","dz" -"Bolivia",591,736,"BO","es" -"Bosnia and Herzegovina",387,218,"BA","bs" -"Botswana",267,652,"BW","en" -"Brazil",55,724,"BR","pt" -"British Columbia", 1250,302,"CA","en" -"British Columbia", 1604,302,"CA","en" -"British Columbia", 1778,302,"CA","en" -"British Indian Ocean Territory",246,348,"IO","en" -"British Virgin Islands",1284,"348","GB","en" -"Brunei",673,528,"BN","ms" -"Bulgaria",359,284,"BG","bg" -"Burkina Faso",226,613,"BF","fr" -"Burundi",257,642,"BI","rn" -"Cambodia",855,456,"KH","km" -"Cameroon",237,624,"CM","fr" -"Cape Verde",238,625,"CV","pt" -"Cayman Islands",1345,"346","GB","en" -"Central African Republic",236,623,"CF","sg" -"Chad",235,622,"TD","fr" -"Chile",56,730,"CL","es" -"China",86,"460|461","CN","en" -"Colombia",57,732,"CO","es" -"Comoros",269,654,"KM","fr" -"Democratic Republic of the Congo",243,630,"CD","fr" -"Republic of the Congo",242,629,"CG","fr" -"Cook Islands",682,548,"CK","en" -"Costa Rica",506,658,"CR","es" -"Cote d'Ivoire",712,"612","CI","fr" -"Croatia",385,219,"HR","hr" -"Cuba",53,368,"CU","es" -"Cyprus",357,280,"CY","el" -"Czech Republic",420,230,"CZ","cs" -"Denmark",45,238,"DK","da" -"Djibouti",253,638,"DJ","fr" -"Dominica",1767,"366","DM","en" -"Dominican Republic",1809,"370","DO","es" -"Dominican Republic",1829,"370","DO","en" -"East Timor",670,514,"TL","pt" -"Ecuador",593,740,"EC","es" -"Egypt",20,602,"EG","ar" -"El Salvador",503,706,"SV","es" -"Equatorial Guinea",240,627,"GQ","es" -"Eritrea",291,657,"ER","ti" -"Estonia",372,248,"EE","et" -"Ethiopia",251,636,"ET","am" -"Falkland Islands",500,750,"FK","en" -"Faroe Islands",298,288,"FO","fo" -"Fiji",679,542,"FJ","en" -"Finland",358,244,"FI","fi" -"France",33,208,"FR","fr" -"French Guiana",594,742,"GF","fr" -"French Polynesia",689,547,"PF","fr" -"Gabon",241,628,"GA","fr" -"Gambia",220,607,"GM","en" -"Gaza Strip",970,0,"PS","ar" -"Georgia",995,282,"GE","ka" -"Germany",49,262,"DE","de" -"Ghana",233,620,"GH","ak" -"Gibraltar",350,266,"GI","en" -"Greece",30,202,"GR","el" -"Greenland",299,290,"GL","kl" -"Grenada",1473,"352","GD","en" -"Guadeloupe",590,340,"GP","fr" -"Guam",1671,"535","GU","en" -"Guatemala",502,704,"GT","es" -"Guinea",224,611,"GN","fr" -"Guinea-Bissau",245,632,"GW","pt" -"Guyana",592,738,"GY","pt" -"Haiti",509,372,"HT","fr" -"Honduras",504,708,"HN","es" -"Hong Kong",852,454,"HK","zh" -"Hungary",36,216,"HU","hu" -"Iceland",354,274,"IS","is" -"India",91,"404|405|406","IN","hi" -"Indonesia",62,510,"ID","id" -"Iraq",964,418,"IQ","ar" -"Iran",98,432,"IR","fa" -"Ireland (Eire)",353,272,"IE","en" -"Israel",972,425,"IL","he" -"Italy",39,222,"IT","it" -"Jamaica",1876,"338","JM","en" -"Japan",81,"440|441","JP","ja" -"Jordan",962,416,"JO","ar" -"Kazakhstan",7,401,"KZ","kk" -"Kenya",254,639,"KE","sw" -"Kiribati",686,545,"KI","en" -"Kuwait",965,419,"KW","ar" -"Kyrgyzstan",996,437,"KG","ky" -"Laos",856,457,"LA","lo" -"Latvia",371,247,"LV","lv" -"Lebanon",961,415,"LB","ar" -"Lesotho",266,651,"LS","st" -"Liberia",231,618,"LR","en" -"Libya",218,606,"LY","ar" -"Liechtenstein",423,295,"LI","de" -"Lithuania",370,246,"LT","lt" -"Luxembourg",352,270,"LU","fr" -"Macau",853,455,"MO","pt" -"Republic of Macedonia",389,294,"MK","mk" -"Madagascar",261,646,"MG","mg" -"Malawi",265,650,"MW","ny" -"Malaysia",60,502,"MY","en" -"Maldives",960,472,"MV","dv" -"Mali",223,610,"ML","fr" -"Malta",356,278,"MT","mt" -"Manitoba",1204,302,"CA","en" -"Marshall Islands",692,551,"MH","mh" -"Martinique",596,340,"MQ","fr" -"Mauritania",222,609,"MR","ar" -"Mauritius",230,617,"MU","en" -"Mayotte",262,654,"YT","fr" -"Mexico",52,334,"MX","es" -"Federated States of Micronesia",691,550,"FM","en" -"Moldova",373,259,"MD","ru" -"Monaco",377,212,"MC","fr" -"Mongolia",976,428,"MN","mn" -"Montenegro",382,297,"ME","sr" -"Montserrat",1664,"354",MS,"en" -"Morocco",212,"604","MA","ar" -"Mozambique",258,643,"MZ","pt" -"Myanmar",95,414,"MM","my" -"Namibia",264,649,"NA","en" -"Nauru",674,536,"NR","na" -"Netherlands",31,204,"NL","nl" -"Netherlands Antilles",599,362,"AN","nl" -"Nepal",977,429,"NP","ne" -"New Brunswick",1506,302,"CA","en" -"New Caledonia",687,546,"NC","fr" -"New Zealand",64,530,"NZ","en" -"Newfoundland",1709,302,"CA","en" -"Nicaragua",505,710,"NI","es" -"Niger",227,614,"NE","fr" -"Nigeria",234,621,"NG","ha" -"Niue",683,555,"NU","en" -"Norfolk Island",6723,505,"NF","en" -"North Korea",850,467,"KP","ko" -"Northern Mariana Islands",1670,"534","MP","en" -"Northwest Territories",1867,302,"CA","en" -"Norway",47,242,"NO","nb" -"Nova Scotia",1902,302,"CA","en" -"Oman",968,422,"OM","ar" -"Ontario",1416,302,"CA","en" -"Ontario",1519,302,"CA","en" -"Ontario",1613,302,"CA","en" -"Ontario",1647,302,"CA","en" -"Ontario",1705,302,"CA","en" -"Ontario",1807,302,"CA","en" -"Ontario",1905,302,"CA","en" -"Pakistan",92,410,"PK","en" -"Palau",680,552,"PW","en" -"Palestine",970,425,"PS","ar" -"Panama",507,714,"PA","es" -"Papua New Guinea",675,537,"PG","ho" -"Paraguay",595,744,"PY","es" -"Peru",51,716,"PE","es" -"Philippines",63,515,"PH","fil" -"Poland",48,260,"PL","pl" -"Portugal",351,268,"PT","pt" -"Qatar",974,427,"QA","ar" -"Quebec",1418,302,"CA","en" -"Quebec",1450,302,"CA","en" -"Quebec",1514,302,"CA","en" -"Quebec",1819,302,"CA","en" -"Reunion",262,647,"RE","fr" -"Romania",40,226,"RO","ro" -"Russia",7,250,"RU","ru" -"Rwanda",250,635,"RW","rw" -"Saint-Barthelemy",590,340,"BL","fr" -"Saint Helena",290,658,"SH","en" -"Saint Kitts and Nevis",1869,"356","KN","en" -"Saint Lucia",1758,"358","LC","en" -"Saint Martin (French side)",590,340, "MF","fr" -"Saint Pierre and Miquelon",508,308,"PM","fr" -"Saint Vincent and the Grenadines",1670,"360","VC","en" -"Samoa",685,549,"WS","sm" -"Sao Tome and Principe",239,626,"ST","pt" -"Saskatchewan",1306,302,"CA","en" -"Saudi Arabia",966,420,"SA","ar" -"Senegal",221,608,"SN","wo" -"Serbia",381,220,"RS","sr" -"Seychelles",248,633,"SC","fr" -"Sierra Leone",232,619,"SL","en" -"Singapore",65,525,"SG","en" -"Slovakia",421,231,"SK","sk" -"Slovenia",386,293,"SI","sl" -"Solomon Islands",677,540,"SB","en" -"Somalia",252,637,"SO","so" -"South Africa",27,655,"ZA","xh" -"South Korea",82,450,"KR","ko" -"South Sudan",211,659,"SS","en" -"Spain",34,214,"es","es" -"Sri Lanka",94,413,"LK","si" -"Sudan",249,634,"SD","ar" -"Suriname",597,746,"SR","nl" -"Swaziland",268,653,"SZ","ss" -"Sweden",46,240,"SE","sv" -"Switzerland",41,228,"CH","de" -"Syria",963,417,"SY","ar" -"Taiwan",886,466,"TW","cmn" -"Tajikistan",992,436,"TJ","tg" -"Tanzania",255,640,"TZ","sw" -"Thailand",66,520,"TH","th" -"Togo",228,615,"TG","fr" -"Tokelau",690,690,"TK","tkl" -"Tonga",676,539,"TO","to" -"Trinidad and Tobago",1868,"374","TT","en" -"Tunisia",216,605,"TN","ar" -"Turkey",90,286,"TR","tr" -"Turkmenistan",993,438,"TM","tk" -"Turks and Caicos Islands",1649,"376","TC","en" -"Tuvalu",688,553,"TV","tvl" -"Uganda",256,641,"UG","sw" -"Ukraine",380,255,"UA","uk" -"United Arab Emirates",971,"424|430|431","AE","ar" -"United Kingdom",44,"234|235","GB","en" -"United States of America",1,"310|311|312|313|314|315|316","en","en" -"Uruguay",598,748,"UY","es" -"Uzbekistan",998,434,"UZ","uz" -"Vanuatu",678,541,"VU","bi" -"Venezuela",58,734,"VE","es" -"Vietnam",84,452,"VN","vi" -"U.S. Virgin Islands",1340,"332","VI","en" -"Wallis and Futuna",681,543,"WF","fr" -"West Bank",970,0,"PS","ar" -"Yemen",967,421,"YE","ar" -"Zambia",260,645,"ZM","en" -"Zimbabwe",263,648,"ZW","en" +"Afghanistan",93,412,"AF","ps","040" +"Albania",355,276,"AL","sq","002" +"Alberta",1403,302,"CA","en","720" +"Alberta",1780,302,"CA","en","720" +"Algeria",213,603,"DZ","ar","001" +"Andorra",376,213,"AD","ca","003" +"Angola",244,631,"AO","pt","002" +"Anguilla",1264,"365","AI","en","840" +"Antarctica (Australian bases)",6721,901,"AQ","en","001" +"Antigua and Barbuda",1268,"344","AG","en","050" +"Argentina",54,722,"AR","es","010" +"Armenia",374,283,"AM","hy","010" +"Aruba",297,363,"AW","nl","001" +"Ascension",247,658,"AC","en","001" +"Australia",61,505,"AU","en","003" +"Austria",43,232,"AT","de","001" +"Azerbaijan",994,400,"AZ","az","001" +"Bahamas",1242,"364","BS","en","039" +"Bahrain",973,426,"BH","ar","001" +"Bangladesh",880,470,"BD","bn","001" +"Barbados",1246,"342","BB","en","750" +"Belarus",375,257,"BY","be","001" +"Belgium",32,206,"BE","nl","001" +"Belize",501,702,"BZ","es","067" +"Benin",229,616,"BJ","fr","003" +"Bermuda",1441,350,"BM","en","001" +"Bhutan",975,402,"BT","dz","011" +"Bolivia",591,736,"BO","es","002" +"Bosnia and Herzegovina",387,218,"BA","bs","003" +"Botswana",267,652,"BW","en","002" +"Brazil",55,724,"BR","pt","002" +"British Columbia", 1250,302,"CA","en","370" +"British Columbia", 1604,302,"CA","en","370" +"British Columbia", 1778,302,"CA","en","370" +"British Indian Ocean Territory",246,348,"IO","en","170" +"British Virgin Islands",1284,"348","GB","en","170" +"Brunei",673,528,"BN","ms","011" +"Bulgaria",359,284,"BG","bg","003" +"Burkina Faso",226,613,"BF","fr","002" +"Burundi",257,642,"BI","rn","001" +"Cambodia",855,456,"KH","km","002" +"Cameroon",237,624,"CM","fr","001" +"Cape Verde",238,625,"CV","pt","001" +"Cayman Islands",1345,"346","GB","en","050" +"Central African Republic",236,623,"CF","sg","001" +"Chad",235,622,"TD","fr","003" +"Chile",56,730,"CL","es","002" +"China",86,"460|461","CN","en","001" +"Colombia",57,732,"CO","es","102" +"Comoros",269,654,"KM","fr","001" +"Democratic Republic of the Congo",243,630,"CD","fr","086" +"Republic of the Congo",242,629,"CG","fr","001" +"Cook Islands",682,548,"CK","en","001" +"Costa Rica",506,712,"CR","es","004" +"Cote d'Ivoire",712,"612","CI","fr","002" +"Croatia",385,219,"HR","hr","001" +"Cuba",53,368,"CU","es","001" +"Cyprus",357,280,"CY","el","001" +"Czech Republic",420,230,"CZ","cs","001" +"Denmark",45,238,"DK","da","001" +"Djibouti",253,638,"DJ","fr","001" +"Dominica",1767,"366","DM","en","020" +"Dominican Republic",1809,"370","DO","es","001" +"Dominican Republic",1829,"370","DO","en","001" +"East Timor",670,514,"TL","pt","001" +"Ecuador",593,740,"EC","es","001" +"Egypt",20,602,"EG","ar","002" +"El Salvador",503,706,"SV","es","001" +"Equatorial Guinea",240,627,"GQ","es","003" +"Eritrea",291,657,"ER","ti","001" +"Estonia",372,248,"EE","et","001" +"Ethiopia",251,636,"ET","am","001" +"Falkland Islands",500,750,"FK","en","001" +"Faroe Islands",298,288,"FO","fo","001" +"Fiji",679,542,"FJ","en","001" +"Finland",358,244,"FI","fi","005" +"France",33,208,"FR","fr","001" +"French Guiana",594,340,"GF","fr","011" +"French Polynesia",689,547,"PF","fr","015" +"Gabon",241,628,"GA","fr","001" +"Gambia",220,607,"GM","en","001" +"Gaza Strip",970,0,"PS","ar","001" +"Georgia",995,282,"GE","ka","001" +"Germany",49,262,"DE","de","001" +"Ghana",233,620,"GH","ak","001" +"Gibraltar",350,266,"GI","en","001" +"Greece",30,202,"GR","el","001" +"Greenland",299,290,"GL","kl","001" +"Grenada",1473,"352","GD","en","030" +"Guadeloupe",590,340,"GP","fr","002" +"Guam",1671,"310","GU","en","032" +"Guatemala",502,704,"GT","es","001" +"Guinea",224,611,"GN","fr","001" +"Guinea-Bissau",245,632,"GW","pt","001" +"Guyana",592,738,"GY","pt","001" +"Haiti",509,372,"HT","fr","001" +"Honduras",504,708,"HN","es","001" +"Hong Kong",852,454,"HK","zh","006" +"Hungary",36,216,"HU","hu","001" +"Iceland",354,274,"IS","is","001" +"India",91,"404|405|406","IN","hi","011" +"Indonesia",62,510,"ID","id","001" +"Iraq",964,418,"IQ","ar","020" +"Iran",98,432,"IR","fa","011" +"Ireland (Eire)",353,272,"IE","en","001" +"Israel",972,425,"IL","he","001" +"Italy",39,222,"IT","it","001" +"Jamaica",1876,"338","JM","en","050" +"Japan",81,"440|441","JP","ja","001" +"Jordan",962,416,"JO","ar","001" +"Kazakhstan",77,401,"KZ","kk","001" +"Kenya",254,639,"KE","sw","002" +"Kiribati",686,545,"KI","en","009" +"Kuwait",965,419,"KW","ar","003" +"Kyrgyzstan",996,437,"KG","ky","001" +"Laos",856,457,"LA","lo","001" +"Latvia",371,247,"LV","lv","001" +"Lebanon",961,415,"LB","ar","003" +"Lesotho",266,651,"LS","st","001" +"Liberia",231,618,"LR","en","007" +"Libya",218,606,"LY","ar","003" +"Liechtenstein",423,295,"LI","de","001" +"Lithuania",370,246,"LT","lt","001" +"Luxembourg",352,270,"LU","fr","001" +"Macau",853,455,"MO","pt","004" +"Republic of Macedonia",389,294,"MK","mk","001" +"Madagascar",261,646,"MG","mg","001" +"Malawi",265,650,"MW","ny","001" +"Malaysia",60,502,"MY","en","013" +"Maldives",960,472,"MV","dv","001" +"Mali",223,610,"ML","fr","001" +"Malta",356,278,"MT","mt","001" +"Manitoba",1204,302,"CA","en","370" +"Marshall Islands",692,551,"MH","mh","001" +"Martinique",596,340,"MQ","fr","001" +"Mauritania",222,609,"MR","ar","002" +"Mauritius",230,617,"MU","en","001" +"Mayotte",262,654,"YT","fr","001" +"Mexico",52,334,"MX","es","030" +"Federated States of Micronesia",691,550,"FM","en","001" +"Moldova",373,259,"MD","ru","001" +"Monaco",377,212,"MC","fr","001" +"Mongolia",976,428,"MN","mn","088" +"Montenegro",382,297,"ME","sr","001" +"Montserrat",1664,"354",MS,"en","860" +"Morocco",212,"604","MA","ar","001" +"Mozambique",258,643,"MZ","pt","001" +"Myanmar",95,414,"MM","my","005" +"Namibia",264,649,"NA","en","001" +"Nauru",674,536,"NR","na","002" +"Netherlands",31,204,"NL","nl","004" +"Netherlands Antilles",599,362,"AN","nl","069" +"Nepal",977,429,"NP","ne","001" +"New Brunswick",1506,302,"CA","en","370" +"New Caledonia",687,546,"NC","fr","001" +"New Zealand",64,530,"NZ","en","001" +"Newfoundland",1709,302,"CA","en","370" +"Nicaragua",505,710,"NI","es","030" +"Niger",227,614,"NE","fr","004" +"Nigeria",234,621,"NG","ha","020" +"Niue",683,555,"NU","en","001" +"Norfolk Island",6723,505,"NF","en","010" +"North Korea",850,467,"KP","ko","005" +"Northern Mariana Islands",1670,"534","MP","en","001" +"Northwest Territories",1867,302,"CA","en","370" +"Norway",47,242,"NO","nb","001" +"Nova Scotia",1902,302,"CA","en","370" +"Oman",968,422,"OM","ar","002" +"Ontario",1416,302,"CA","en","370" +"Ontario",1519,302,"CA","en","370" +"Ontario",1613,302,"CA","en","370" +"Ontario",1647,302,"CA","en","370" +"Ontario",1705,302,"CA","en","370" +"Ontario",1807,302,"CA","en","370" +"Ontario",1905,302,"CA","en","370" +"Pakistan",92,410,"PK","en","001" +"Palau",680,552,"PW","en","001" +"Palestine",970,425,"PS","ar","006" +"Panama",507,714,"PA","es","002" +"Papua New Guinea",675,537,"PG","ho","001" +"Paraguay",595,744,"PY","es","001" +"Peru",51,716,"PE","es","006" +"Philippines",63,515,"PH","fil","002" +"Poland",48,260,"PL","pl","001" +"Portugal",351,268,"PT","pt","001" +"Qatar",974,427,"QA","ar","001" +"Quebec",1418,302,"CA","en","370" +"Quebec",1450,302,"CA","en","370" +"Quebec",1514,302,"CA","en","370" +"Quebec",1819,302,"CA","en","370" +"Reunion",262,647,"RE","fr","002" +"Romania",40,226,"RO","ro","001" +"Russia",79,250,"RU","ru","001" +"Rwanda",250,635,"RW","rw","013" +"Saint-Barthelemy",590,340,"BL","fr","001" +"Saint Helena",290,658,"SH","en","000" +"Saint Kitts and Nevis",1869,"356","KN","en","050" +"Saint Lucia",1758,"358","LC","en","050" +"Saint Martin (French side)",590,340, "MF","fr","001" +"Saint Pierre and Miquelon",508,308,"PM","fr","001" +"Saint Vincent and the Grenadines",1670,"360","VC","en","070" +"Samoa",685,549,"WS","sm","001" +"Sao Tome and Principe",239,626,"ST","pt","001" +"Saskatchewan",1306,302,"CA","en","370" +"Saudi Arabia",966,420,"SA","ar","001" +"Senegal",221,608,"SN","wo","001" +"Serbia",381,220,"RS","sr","001" +"Seychelles",248,633,"SC","fr","001" +"Sierra Leone",232,619,"SL","en","001" +"Singapore",65,525,"SG","en","001" +"Slovakia",421,231,"SK","sk","001" +"Slovenia",386,293,"SI","sl","031" +"Solomon Islands",677,540,"SB","en","001" +"Somalia",252,637,"SO","so","001" +"South Africa",27,655,"ZA","xh","001" +"South Korea",82,450,"KR","ko","005" +"South Sudan",211,659,"SS","en","002" +"Spain",34,214,"ES","es","007" +"Sri Lanka",94,413,"LK","si","001" +"Sudan",249,634,"SD","ar","001" +"Suriname",597,746,"SR","nl","002" +"Swaziland",268,653,"SZ","ss","010" +"Sweden",46,240,"SE","sv","001" +"Switzerland",41,228,"CH","de","001" +"Syria",963,417,"SY","ar","001" +"Taiwan",886,466,"TW","cmn","001" +"Tajikistan",992,436,"TJ","tg","001" +"Tanzania",255,640,"TZ","sw","002" +"Thailand",66,520,"TH","th","018" +"Togo",228,615,"TG","fr","001" +"Tokelau",690,690,"TK","tkl","001" +"Tonga",676,539,"TO","to","001" +"Trinidad and Tobago",1868,"374","TT","en","012" +"Tunisia",216,605,"TN","ar","001" +"Turkey",90,286,"TR","tr","001" +"Turkmenistan",993,438,"TM","tk","001" +"Turks and Caicos Islands",1649,"376","TC","en","350" +"Tuvalu",688,553,"TV","tvl","001" +"Uganda",256,641,"UG","sw","001" +"Ukraine",380,255,"UA","uk","001" +"United Arab Emirates",971,"424|430|431","AE","ar","003" +"United Kingdom",44,"234|235","GB","en","002" +"United States of America",1,"310|311|312|313|314|315|316","US","en","150" +"Uruguay",598,748,"UY","es","007" +"Uzbekistan",998,434,"UZ","uz","001" +"Vanuatu",678,541,"VU","bi","005" +"Venezuela",58,734,"VE","es","004" +"Vietnam",84,452,"VN","vi","001" +"U.S. Virgin Islands",1340,"332","VI","en","001" +"Wallis and Futuna",681,543,"WF","fr","001" +"West Bank",970,425,"PS","ar","001" +"Yemen",967,421,"YE","ar","001" +"Zambia",260,645,"ZM","en","001" +"Zimbabwe",263,648,"ZW","en","001" diff --git a/src/decode.php b/src/decode.php deleted file mode 100755 index 04c710e1..00000000 --- a/src/decode.php +++ /dev/null @@ -1,42 +0,0 @@ - $v) { - $str .= " " . getToken(hexdec($v)); - } - - return $str; -} - -function str2hex($string) -{ - $hexstr = unpack('H*', $string); - - return array_shift($hexstr); -} - -function hex2str($hexstr) -{ - $hexstr = str_replace(' ', '', $hexstr); - $hexstr = str_replace('\x', '', $hexstr); - $retstr = pack('H*', $hexstr); - - return $retstr; -} - -function printhexstr($data, $name) -{ - $data = str2hex($data); - $len = strlen($data); - print("Len: $len - $name\n"); - for ($i = 0; $i < $len; $i += 2) { - if ((($i - 1) % 32) == 31) { - print("\n"); - } - printf(" %s%s", $data[$i], $data[$i + 1]); - } - - print("\n"); -} diff --git a/src/events/AllEvents.php b/src/events/AllEvents.php new file mode 100644 index 00000000..d081baa0 --- /dev/null +++ b/src/events/AllEvents.php @@ -0,0 +1,358 @@ +whatsProt = $whatsProt; + + return $this; + } + + /** + * Register the events you want to listen for. + * + * @param array $eventList + * + * @return AllEvents + */ + public function setEventsToListenFor(array $eventList) + { + $this->eventsToListenFor = $eventList; + + return $this->startListening(); + } + + /** + * Binds the requested events to the event manager. + * + * @return $this + */ + protected function startListening() + { + foreach ($this->eventsToListenFor as $event) { + if (is_callable([$this, $event])) { + $this->whatsProt->eventManager()->bind($event, [$this, $event]); + } + } + + return $this; + } + + //Adding to this list? Please put them in alphabetical order! + + public function onCallReceived($mynumber, $from, $id, $notify, $time, $callId) + { + } + + public function onClose($mynumber, $error) + { + } + + public function onCodeRegister($mynumber, $login, $password, $type, $expiration, $kind, $price, $cost, $currency, $price_expiration) + { + } + + public function onCodeRegisterFailed($mynumber, $status, $reason, $retry_after) + { + } + + public function onCodeRequest($mynumber, $method, $length) + { + } + + public function onCodeRequestFailed($mynumber, $method, $reason, $param) + { + } + + public function onCodeRequestFailedTooRecent($mynumber, $method, $reason, $retry_after) + { + } + + public function onCodeRequestFailedTooManyGuesses($mynumber, $method, $reason, $retry_after) + { + } + + public function onConnect($mynumber, $socket) + { + } + + public function onConnectError($mynumber, $socket) + { + } + + public function onCredentialsBad($mynumber, $status, $reason) + { + } + + public function onCredentialsGood($mynumber, $login, $password, $type, $expiration, $kind, $price, $cost, $currency, $price_expiration) + { + } + + public function onDisconnect($mynumber, $socket) + { + } + + public function onDissectPhone($mynumber, $phonecountry, $phonecc, $phone, $phonemcc, $phoneISO3166, $phoneISO639, $phonemnc) + { + } + + public function onDissectPhoneFailed($mynumber) + { + } + + public function onGetAudio($mynumber, $from, $id, $type, $time, $name, $size, $url, $file, $mimeType, $fileHash, $duration, $acodec) + { + } + + public function onGetBroadcastLists($mynumber, $broadcastLists) + { + } + + public function onGetError($mynumber, $from, $id, $data, $errorType = null) + { + } + + public function onGetExtendAccount($mynumber, $kind, $status, $creation, $expiration) + { + } + + public function onGetFeature($mynumber, $from, $encrypt) + { + } + + public function onGetGroupMessage($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time, $name, $body) + { + } + + public function onGetGroups($mynumber, $groupList) + { + } + + public function onGetGroupV2Info($mynumber, $group_id, $creator, $creation, $subject, $participants, $admins, $fromGetGroup) + { + } + + public function onGetGroupsSubject($mynumber, $group_jid, $time, $author, $name, $subject) + { + } + + public function onGetImage($mynumber, $from, $id, $type, $time, $name, $size, $url, $file, $mimeType, $fileHash, $width, $height, $preview, $caption) + { + } + + public function onGetGroupAudio($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time, $name, $size, $url, $file, $mimeType, $fileHash, $duration, $acodec) + { + } + + public function onGetGroupImage($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time, $name, $size, $url, $file, $mimeType, $fileHash, $width, $height, $preview, $caption) + { + } + + public function onGetGroupLocation($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time, $name, $author, $longitude, $latitude, $url, $preview) + { + } + + public function onGetGroupVideo($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time, $name, $url, $file, $size, $mimeType, $fileHash, $duration, $vcodec, $acodec, $preview, $caption) + { + } + + public function onGetGroupvCard($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time, $name, $vcardname, $vcard) + { + } + + public function onGetLocation($mynumber, $from, $id, $type, $time, $name, $author, $longitude, $latitude, $url, $preview) + { + } + + public function onGetMessage($mynumber, $from, $id, $type, $time, $name, $body) + { + } + + public function onGetNormalizedJid($mynumber, $data) + { + } + + public function onGetPrivacyBlockedList($mynumber, $data) + { + } + + public function onGetProfilePicture($mynumber, $from, $type, $data) + { + } + + public function onGetReceipt($from, $id, $offline, $retry) + { + } + + public function onGetServerProperties($mynumber, $version, $props) + { + } + + public function onGetServicePricing($mynumber, $price, $cost, $currency, $expiration) + { + } + + public function onGetStatus($mynumber, $from, $requested, $id, $time, $data) + { + } + + public function onGetSyncResult($result) + { + } + + public function onGetVideo($mynumber, $from, $id, $type, $time, $name, $url, $file, $size, $mimeType, $fileHash, $duration, $vcodec, $acodec, $preview, $caption) + { + } + + public function onGetvCard($mynumber, $from, $id, $type, $time, $name, $vcardname, $vcard) + { + } + + public function onGroupCreate($mynumber, $groupId) + { + } + + public function onGroupisCreated($mynumber, $creator, $gid, $subject, $admin, $creation, $members = []) + { + } + + public function onGroupsChatCreate($mynumber, $gid) + { + } + + public function onGroupsChatEnd($mynumber, $gid) + { + } + + public function onGroupsParticipantsAdd($mynumber, $groupId, $jid) + { + } + + public function onGroupsParticipantChangedNumber($mynumber, $groupId, $time, $oldNumber, $notify, $newNumber) + { + } + + public function onGroupsParticipantsPromote($myNumber, $groupJID, $time, $issuerJID, $issuerName, $promotedJIDs = []) + { + } + + public function onGroupsParticipantsRemove($mynumber, $groupId, $jid) + { + } + + public function onLoginFailed($mynumber, $data) + { + } + + public function onLoginSuccess($mynumber, $kind, $status, $creation, $expiration) + { + } + + public function onAccountExpired($mynumber, $kind, $status, $creation, $expiration) + { + } + + public function onMediaMessageSent($mynumber, $to, $id, $filetype, $url, $filename, $filesize, $filehash, $caption, $icon) + { + } + + public function onMediaUploadFailed($mynumber, $id, $node, $messageNode, $statusMessage) + { + } + + public function onMessageComposing($mynumber, $from, $id, $type, $time) + { + } + + public function onMessagePaused($mynumber, $from, $id, $type, $time) + { + } + + public function onGroupMessageComposing($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time) + { + } + + public function onGroupMessagePaused($mynumber, $from_group_jid, $from_user_jid, $id, $type, $time) + { + } + + public function onMessageReceivedClient($mynumber, $from, $id, $type, $time, $participant) + { + } + + public function onMessageReceivedServer($mynumber, $from, $id, $type, $time) + { + } + + public function onNumberWasAdded($mynumber, $jid) + { + } + + public function onNumberWasRemoved($mynumber, $jid) + { + } + + public function onNumberWasUpdated($mynumber, $jid) + { + } + + public function onPaidAccount($mynumber, $author, $kind, $status, $creation, $expiration) + { + } + + public function onPaymentRecieved($mynumber, $kind, $status, $creation, $expiration) + { + } + + public function onPing($mynumber, $id) + { + } + + public function onPresenceAvailable($mynumber, $from) + { + } + + public function onPresenceUnavailable($mynumber, $from, $last) + { + } + + public function onProfilePictureChanged($mynumber, $from, $id, $time) + { + } + + public function onProfilePictureDeleted($mynumber, $from, $id, $time) + { + } + + public function onSendMessage($mynumber, $target, $messageId, $node) + { + } + + public function onSendMessageReceived($mynumber, $id, $from, $type) + { + } + + public function onSendPong($mynumber, $msgid) + { + } + + public function onSendPresence($mynumber, $type, $name) + { + } + + public function onSendStatusUpdate($mynumber, $txt) + { + } + + public function onStreamError($data) + { + } + + public function onWebSync($mynumber, $from, $id, $syncData, $code, $name) + { + } +} diff --git a/src/events/MyEvents.php b/src/events/MyEvents.php new file mode 100644 index 00000000..2a7b9e02 --- /dev/null +++ b/src/events/MyEvents.php @@ -0,0 +1,90 @@ +WooHoo!, Phone number $mynumber connected successfully!

"; + } + + public function onDisconnect($mynumber, $socket) + { + echo "

Booo!, Phone number $mynumber is disconnected!

"; + } +} diff --git a/src/events/WhatsApiEventsManager.php b/src/events/WhatsApiEventsManager.php new file mode 100644 index 00000000..ed29d397 --- /dev/null +++ b/src/events/WhatsApiEventsManager.php @@ -0,0 +1,20 @@ +listeners[$event][] = $callback; + } + + public function fire($event, array $parameters) + { + if (!empty($this->listeners[$event])) { + foreach ($this->listeners[$event] as $listener) { + call_user_func_array($listener, $parameters); + } + } + } +} diff --git a/src/events/WhatsAppEventListener.php b/src/events/WhatsAppEventListener.php deleted file mode 100755 index 41b73dc2..00000000 --- a/src/events/WhatsAppEventListener.php +++ /dev/null @@ -1,435 +0,0 @@ -eventName = $eventName; - $this->callback = $callback; - } - - protected function handleEvent($eventName, array $arguments) - { - if( $this->eventName === $eventName ) { - call_user_func_array( $this->callback, $arguments ); - } - } - -} - diff --git a/src/events/WhatsAppEventListenerProxy.php b/src/events/WhatsAppEventListenerProxy.php deleted file mode 100755 index 710331e7..00000000 --- a/src/events/WhatsAppEventListenerProxy.php +++ /dev/null @@ -1,556 +0,0 @@ -handleEvent(__FUNCTION__, func_get_args()); - } - - function onCodeRegister( - $phone, - $login, - $pw, - $type, - $expiration, - $kind, - $price, - $cost, - $currency, - $price_expiration - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onCodeRegisterFailed( - $phone, - $status, - $reason, - $retry_after - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onCodeRequest( - $phone, - $method, - $length - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onCodeRequestFailed( - $phone, - $method, - $reason, - $value - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onCodeRequestFailedTooRecent( - $phone, - $method, - $reason, - $retry_after - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onConnect( - $phone, - $socket - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onCredentialsBad( - $phone, - $status, - $reason - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onCredentialsGood( - $phone, - $login, - $pw, - $type, - $expiration, - $kind, - $price, - $cost, - $currency, - $price_expiration - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onDisconnect( - $phone, - $socket - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onDissectPhone( - $phone, - $country, - $cc, - $mcc, - $lc, - $lg - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onDissectPhoneFailed( - $phone - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetAudio( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $size, - $url, - $file, - $mimetype, - $filehash, - $duration, - $acodec - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetError( - $phone, - $id, - $error - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetGroups( - $phone, - $groupList - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetGroupsInfo( - $phone, - $groupList - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetGroupsSubject( - $phone, - $gId, - $time, - $author, - $participant, - $name, - $subject - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetImage( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $size, - $url, - $file, - $mimetype, - $filehash, - $width, - $height, - $thumbnail - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetLocation( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $place_name, - $longitude, - $latitude, - $url, - $thumbnail - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetMessage( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $message - ) { - $func = __FUNCTION__; - $args = func_get_args(); - $this->handleEvent($func, $args); - } - - function onGetGroupMessage( - $phone, - $from, - $author, - $msgid, - $type, - $time, - $name, - $message - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetPrivacyBlockedList( - $phone, - $children - /* - $data, - $onGetProfilePicture, - $phone, - $from, - $type, - $thumbnail - */ - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetProfilePicture( - $phone, - $from, - $type, - $thumbnail - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetRequestLastSeen( - $phone, - $from, - $msgid, - $sec - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetServerProperties( - $phone, - $version, - $properties - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetvCard( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $contact, - $vcard - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGetVideo( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $url, - $file, - $size, - $mimetype, - $filehash, - $duration, - $vcodec, - $acodec, - $thumbnail - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGroupsChatCreate( - $phone, - $gId - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGroupsChatEnd( - $phone, - $gId - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGroupsParticipantsAdd( - $phone, - $groupId, - $participant - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onGroupsParticipantsRemove( - $phone, - $groupId, - $participant, - $author - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onLogin( - $phone - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onMessageComposing( - $phone, - $from, - $msgid, - $type, - $time - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onMessagePaused( - $phone, - $from, - $msgid, - $type, - $time - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onMessageReceivedClient( - $phone, - $from, - $msgid, - $type, - $time - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onMessageReceivedServer( - $phone, - $from, - $msgid, - $type - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onPing( - $phone, - $msgid - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onPresence( - $phone, - $from, - $type - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onSendMessageReceived( - $phone, - $id, - $from, - $type - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onSendPong( - $phone, - $msgid - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onSendPresence( - $phone, - $type, - $name - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onSendStatusUpdate( - $phone, - $msg - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onUploadFile( - $phone, - $name, - $url - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - function onUploadFileFailed( - $phone, - $name - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onConnectError( - $phone, - $socket - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onGetGroupParticipants( - $phone, - $groupId, - $groupList - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onGetStatus( - $phone, - $from, - $type, - $id, - $t, - $status - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onLoginFailed( - $phone, - $tag - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onMediaMessageSent( - $phone, - $to, - $id, - $filetype, - $url, - $filename, - $filesize, - $filehash, - $icon - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onMediaUploadFailed( - $phone, - $id, - $node, - $messageNode, - $reason - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onProfilePictureChanged( - $phone, - $from, - $id, - $t - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onProfilePictureDeleted( - $phone, - $from, - $id, - $t - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onSendMessage( - $phone, - $targets, - $id, - $node - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - /** - * @param SyncResult $result - * @return mixed|void - */ - public function onGetSyncResult( - $result - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - - public function onGetReceipt( - $from, - $id, - $offline, - $retry - ) { - $this->handleEvent(__FUNCTION__, func_get_args()); - } - -} diff --git a/src/exception.php b/src/exception.php index 0436c5b4..1a780d6c 100755 --- a/src/exception.php +++ b/src/exception.php @@ -1,16 +1,39 @@ message}' in {$this->file}({$this->line})\n" - . "{$this->getTraceAsString()}"; + return get_class($this)." '{$this->message}' in {$this->file}({$this->line})\n" + ."{$this->getTraceAsString()}"; } +} +/* + * Exception occurs when we have no active socket + * connection to whatsapp + */ +class ConnectionException extends Exception +{ +} + +class LoginFailureException extends Exception +{ } diff --git a/src/func.php b/src/func.php index f62c346b..faba2be0 100755 --- a/src/func.php +++ b/src/func.php @@ -1,43 +1,31 @@ - * this will show the smiling face, require emojisprite.css and emojisprite.png - * - * Otherwise, if it is "false", return an id. Example, ##1F604##. - * - * @return string - */ -function ParseMessageInboundForEmojis($txt, $span = true) { - $Emojis = ArrayEmojis(); - foreach ($Emojis as $Emoji) { - $txt = str_replace( - array($Emoji['iOS2'], $Emoji['iOS5'], $Emoji['iOS7']), - (($span == true) ? '##'.$Emoji['Hex'].'##' : '##'.$Emoji['Hex'].'##'), - $txt - ); - } - return $txt; -} + /** * This function extracts the phone number. * * @param string $from - * The remitter delivered by WHATSAPP example 1234567890@s.whatsapp.net + * The remitter delivered by WHATSAPP example 1234567890@s.whatsapp.net * * @return string - * Returns the number of phone cleanly. - * -**/ -function ExtractNumber($from){ - return str_replace(array("@s.whatsapp.net","@g.us"), "", $from); + * Returns the number of phone cleanly. + **/ +function ExtractNumber($from) +{ + return str_replace(['@s.whatsapp.net', '@g.us'], '', $from); } +function pkcs5_unpad($text) +{ + $pad = ord($text{strlen($text) - 1}); + if ($pad > strlen($text)) { + return false; + } + if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) { + return false; + } + + return substr($text, 0, -1 * $pad); +} + function wa_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false) { $algorithm = strtolower($algorithm); @@ -48,12 +36,12 @@ function wa_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_outpu die('PBKDF2 ERROR: Invalid parameters.'); } - $hash_length = strlen(hash($algorithm, "", true)); + $hash_length = strlen(hash($algorithm, '', true)); $block_count = ceil($key_length / $hash_length); - $output = ""; + $output = ''; for ($i = 1; $i <= $block_count; $i++) { - $last = $salt . pack("N", $i); + $last = $salt.pack('N', $i); $last = $xorsum = hash_hmac($algorithm, $last, $password, true); for ($j = 1; $j < $count; $j++) { $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true)); @@ -71,58 +59,71 @@ function wa_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_outpu function preprocessProfilePicture($path) { list($width, $height) = getimagesize($path); - if ($width != $height) { - throw new Exception("Profile picture needs to be square (image is $width x $height)"); - } - if ($width > 640) { - throw new Exception("Profile picture maximum size of 640 x 640 (image is $width x $height)"); + if ($width > $height) { + $y = 0; + $x = ($width - $height) / 2; + $smallestSide = $height; + } else { + $x = 0; + $y = ($height - $width) / 2; + $smallestSide = $width; } - $img = imagecreatefromjpeg($path); - unlink($path); - imagejpeg($img, $path, 50); + + $size = 639; + $image = imagecreatetruecolor($size, $size); + $img = imagecreatefromstring(file_get_contents($path)); + + imagecopyresampled($image, $img, 0, 0, $x, $y, $size, $size, $smallestSide, $smallestSide); + ob_start(); + imagejpeg($image); + $i = ob_get_contents(); + ob_end_clean(); + + imagedestroy($image); imagedestroy($img); + + return $i; } function createIcon($file) { - if ((extension_loaded('gd')) && (file_exists($file))){ + if ((extension_loaded('gd')) && (file_exists($file))) { return createIconGD($file); } else { - return giftThumbnail(); + return base64_decode(giftThumbnail()); } } -function createIconGD($file, $size = 100, $raw = false) +function createIconGD($file, $size = 100, $raw = true) { list($width, $height) = getimagesize($file); if ($width > $height) { - //landscape - $nheight = ($height / $width) * $size; - $nwidth = $size; + $y = 0; + $x = ($width - $height) / 2; + $smallestSide = $height; } else { - $nwidth = ($width / $height) * $size; - $nheight = $size; + $x = 0; + $y = ($height - $width) / 2; + $smallestSide = $width; } - $image_p = imagecreatetruecolor($nwidth, $nheight); - $image = imagecreatefromjpeg($file); - imagecopyresampled($image_p, $image, 0, 0, 0, 0, $nwidth, $nheight, $width, $height); + + $image_p = imagecreatetruecolor($size, $size); + $image = imagecreatefromstring(file_get_contents($file)); + + imagecopyresampled($image_p, $image, 0, 0, $x, $y, $size, $size, $smallestSide, $smallestSide); ob_start(); imagejpeg($image_p); $i = ob_get_contents(); ob_end_clean(); - if ($raw) { - return $i; - } else { - return base64_encode($i); - } + + imagedestroy($image); + imagedestroy($image_p); + + return $i; } function createVideoIcon($file) { - // @todo: Add support for video thumbnail create. - // @see: http://stackoverflow.com/questions/14662027/generate-thumbnail-for-a-bunch-of-mp4-video-in-a-folder - //return giftThumbnail(); - /* should install ffmpeg for the method to work successfully */ if (checkFFMPEG()) { //generate thumbnail @@ -130,26 +131,24 @@ function createVideoIcon($file) @unlink($preview); //capture video preview - $command = "ffmpeg -i \"" . $file . "\" -f image2 -vframes 1 \"" . $preview . "\""; + $command = 'ffmpeg -i "'.$file.'" -f mjpeg -ss 00:00:01 -vframes 1 "'.$preview.'"'; exec($command); - // Parsear la imagen - //TODO: Make it work using libGD (see createIcon()) return createIconGD($preview); } else { - //fallback - return giftThumbnail(); + return base64_decode(videoThumbnail()); } } function checkFFMPEG() { - //check if ffmpeg is intalled - $cmd = "ffmpeg -version"; - $res = exec($cmd, $output, $returnvalue); - if($returnvalue == 0) - return true; - return false; + //check if ffmpeg is installed. + $output = []; + $returnvalue = false; + + exec('ffmpeg -version', $output, $returnvalue); + + return $returnvalue === 0; } function giftThumbnail() @@ -161,31 +160,242 @@ function videoThumbnail() { return '/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABQAAD/4QMpaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjAtYzA2MCA2MS4xMzQ3NzcsIDIwMTAvMDIvMTItMTc6MzI6MDAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2MTQyRUVCOEI3MDgxMUUyQjNGQkY1OEU5M0U2MDE1MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2MTQyRUVCOUI3MDgxMUUyQjNGQkY1OEU5M0U2MDE1MyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjYxNDJFRUI2QjcwODExRTJCM0ZCRjU4RTkzRTYwMTUzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjYxNDJFRUI3QjcwODExRTJCM0ZCRjU4RTkzRTYwMTUzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAAgICAgICAgICAgMCAgIDBAMCAgMEBQQEBAQEBQYFBQUFBQUGBgcHCAcHBgkJCgoJCQwMDAwMDAwMDAwMDAwMDAEDAwMFBAUJBgYJDQsJCw0PDg4ODg8PDAwMDAwPDwwMDAwMDA8MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/8AAEQgAZABkAwERAAIRAQMRAf/EALUAAAEEAwEBAQAAAAAAAAAAAAAGBwgJBAUKAwECAQEAAQUBAQAAAAAAAAAAAAAAAwECBAYHBQgQAAAFBAADBAUFDQkBAAAAAAECAwQFABEGByESCDFRIhNBYTIUCYEz07QVcZGhUmKCkqIjZWZ2OHKzJHSEJZUWNhcRAAIBAQMHCAgEBwEAAAAAAAABAgMRBAUhMdGScwYWQVGRseFSJAdhcaEiQrLSNcESE1PwgTJyI2M0F//aAAwDAQACEQMRAD8Av8oAoAoAoAoAoAoAoAoAoAoAoAoAoAoAoAoDHcPGjQomdOkWxShzGMqcpAAA9NzCHCgEe/2draKAxpPYONx4E4n95lWaVv01QoDBLtvXKxUDsspbSyTohTtV4wiz9NUpvZEijVNUpr+oaAZCa64umuEUXRPnLqScN1DJKt2ENKLmBQhuUxBN7qBQEBCw3GgHTf7gTaRDibb6+y5+ybslXwnK0aoiZJFIVjAQF3SYiblDgFqAhEx+KDrybyTHsbhNaThFckkmka0fzMnFx6CZ3ipUiHV8tdyYCgJuPC9APDvbZG/sS1lnubYkrBMZPDY5zJfYibhJ44VTbiAqcoHYn4JkAxh8Ijw7KAxOgHqKyfqK1FMTObyjWZy7GZ5aPkZVmVJNFw3WSI5anKmig2KSxTiQfBx5b3G9ATpoBP5ZKuYLFslm2ZElHkPFPHzVNfm8oyjdA6pAU5fFyiJQvbjagKXMj+IbmSbhw1hMhlpVqkPISTJFRkcCwhwMciShHByFEfZAxxNbt40A9nSx1JOd2LZxCZdO5QTI4b3eQimycr7sVePU/ZKmKDYEuKatuYA9BgoBout3Mdga5m8SnsYeShsNyVmZgsR1MSSoN5RpcxiiALAWyyRgMHrKNALPoj21D7HwvIseyLGYJ7m2GSBnKzpw3FZRzHPx5kVv2hhNdJQDJiN/xe+gIpddkJlWv9poZTDqlY4ns1sLxqmi2TBNvJNClSeNwMJRHxF5VShfsEe6gJndDu8pLYmm20JIySSeU6oXJCv1OVFJRSPOAqR7oR5QvcvMmI/jF9dAVz9Yelswg93zymDNJjJsWz9McgjEIQyz4GbhwYSvWhyNRP5YlWATkAQDwm4dlAWrdLWV7AyjS2HG2PjWTQ2bY0QYOXbSsRKCo/TZABWz0pfdzc5VkeUDflANAVi7o+Hdvd3t3OSaj1s7kNfyz77WxiROu0jwalff4g7YAdrInAWyphAvh7LekKAuT1hi24VNc4c32fg5i5w2iUY7MkE3sc4bO1UieQdUDlciBgXTABOAh2iIcaAxujTpaddMEZtSOPKouonOcpPLYxDpAJlI2NIUwIN11fZOoHOIDyXKBSl4iIjYCaVAJ/LW6jvFcmaIoi5VdRL1JJuWwioY6BygUL8OIjbjQHNDvpzgLrNwVxpvjaavlOCz44CVVvBmODo/uQpJugMAOAa8oORSAExU4l9NALHo/SScdQOF/wDX2kuko2RfLZA597S8r7MBAQcFUICPEDmEgBx9q1AT363ZbA4zQj1DIIl9MO5ObjkcbYqO00hF4mcVTrAYiQGAE0SnvbtvagIh9CCMPNbinZGGxd7AxUFjDks/IN5AwmU98VIm2bjzJiA3OUTcezlvQD+9cWdYtrHFMCbQ7B87yzIZpd21I+dIuwRYtERKuqBHCKpScx1CFASlAaAxOhrYGU7RPsKeyf7RJjGNFYxkGZsug1N9pK86y3lmbt0hHlR5QG4j20BreuHqayjUeT4FhuucyyuHlV4tzL5QmjLAYARXUKmyKYFEj2MPlnMFrcKAdfoo2LsbZ2scgzrZGW5fKISOQKMsSVPMGTN7sySKRyYPKTTKJRWMIAIgPZQETes/q22DgG7HuC6y2TmcLH4zDsksiQSl01SBJuAM4UDmWRUMAkSOmBgva/ooCc3SXkme5fojDMt2ZluZS+S5eo7k2LlSYMkqMcusJWRTFSTTLxIXmCxb2MF6AVXSXuyT2hvDqsxhtk8pPYHryWhWGJtZRYjszZcEnLeQMg55QUMmou3EQKcTWELltegJ+0B+FEyKpnSULzJqFEpyj6QELCFAcpmzcWLj21NhYtDFI3gYDIpaPiET3OqRu0dKJpFOe4cwgUoAI241tW5+B0MYvkqFdyUVBy92xO1NLlTyZTwN48Xnhd2jVgk25KOX1N/gLHTW0Mv0hKzs3isXByknPs02C7mXRWVFBBNTzRKj5Sqduc1uYRv2BXRX5a4Z+5V6Y/SaZHf28csI9D0mXunbuwd8BjieXkiYxpjHvBmDGIRVSSOq55QOqqCqqgiYClAoWtYKs/8AN8N/cq9MfpL+O7xyQj0PSZ+lty51omMnozD4bHX45I7SdycjLILquB8hMU0kiiksmAELcRtbtEatflxhv7lXpj9Jct+rx3I9D0iX3FlWZ75ydjlOZqsmTqMjiRkdHRaZ02qKJTmUMYpVTqG5lDmuYb+gKjfl3hq+Or0x+kuW/F47kPbpHa07u3YmlsLQwXD4bGl4sj1zIuH0g2cKO3Dl0ICc6p01yFHlApSlsHAAqN+X2HL46vTH6S5b7XjuQ9ukZzaMLO7lzqc2Fl0mCM7PAgRVuwTAjVuk2SBFJFAignMBSlC/ER4iI1G9wcOXx1elaCq30vHch7dJJvXG/dl6wwrFcBxeExT7AxBoVnHe9M3B1lQA4qHVXMVwUDHUOYTGEACrHuHh/fqdK0F63zvHch7dJEjLtPq5/k+T5dkeQPHE3mEi5k5pZMCFKZZ0fmOUlwEQKUPCUL8AAKs4Fw/v1OlaCvGVfuR9ukmybqd3BiuHCyg4bEI5li8IRjBpkZOQBuk1QBBAS3c25iAACH5VY1+3LuNC7VKsZVPzRg5LKrLUrcuTMTXTe6tWr06bhGyUkuXlfrHm+D7jccjqHaOaqpnWyrI8yOxmpVQ5jCsiybpuEgEo8AHzXixjD6eb1BXL07Ub+85bzVSgUBy9boNy7v2uP8Xz311auheWn3Kpsn8yNK39VtwhtF1MQAKca7W2cnUT1BSo2yVRMlI17CI2KHC49ny1FJkiiKBsTsrHlIlUTfN06x5SJFE3aCXZwqGUi9I3DdsJrcKjbK22G5TbJpJmVVMVNMgXOc3AAqiI2xu84fKPoKVTQAyTFJHmsPAVBAweI3q7gqDE4fluNfn/AE59Rl4XLxtHaR6yxj4RX9Pmcfz8++pM6+e45kdxlnZaxVxQKA5dN3m5d27WH+MZ764tXQfLX7lU2T+ZGm79K24w2i6mNuCldpbOVqJ7EPcQCopMlUR9tIIIOMgmEHCCblBSKEFEVSFOQweaXtKYBCvGxebVOLTsy/gZ9yinJ28w9EnqjGJLmVYFVgnJuIC28SN/WibgH5ohXlQxKrDP7y9OkzJ3OEs2QQkhrDJ4q526BJpsXj5zP27B3pG8QfJesuGIU558j9Okxp3WcfSadszOU4pqkMmoUbGSOUSmAfWUbCFZDdpjt2G4FRuyKHmjzqdpUC+0P3e75aKLZE5GrcC5fmAVfCkXimgX2Q9Y94+upoxUSNyE9lbLy8Wnj29loYf1i1h4q/A19nPqZl4VLxtDaR6ywf4RP9Pmcfz8++pM6+eY5kd2lnZaxVxQKA5b96m5d1bVH+Mp364tW/8Alv8AcqmyfzI0/fdW3GG0XUxrQU9ddnbOYqJmtjcxr1FJkiiSG0V/6WV9cWP96WvFxd/416zOuUfefqJYtyXtWutnpG2TOikUTmOFicREPR90ewKtsbLWxD5PleErFO2fkRmXIBy+W0KB1Sj/AJgtgL9+s+7XWussfdXp0GFXr0fiyv8AjlGUO1ZqulVGDZVq1ON0kFlfOUL/AGj2C/3q9yP5kvedr6DyJyTeTIjZosOAcPRVbSFyNJm7Ly8Myc9rcrA4/rFrBxR+Cr7Ofysy8KfjaG0j1k2/hE/0+Zx/Pz76kyr58jmR3yWdlrFXFAoDlp34Ntz7UH+M5364vW/+XH3Kpsn8yNS30/4obRdTGhFyQnaYa7KzmkUZjV+QtgAhjD8gVFJEqiPnp/J46DmpV/MOk41mMaKaahwMcx1BUKIEKUoCIjYOyvLxGhKrBKKtdpkXecYSbb5B2pHcwKCKWPxh1Q7CvX48pfulRIN/0hrDpYTyzf8AJaRUvy+FdIkHU/Pz5ry0ms4SEfC1IPlol9QJksH3716FO706X9K0nn1a8p52bNg1AAAAKAB6ACr2YzkKto1CxfDUbI2xRt2YCHs1Y2WNie2E0AmBZce1uWNUG/5xawsTfg6+zn8rM3Cn46htI9ZLL4RP9Pecfz6++pMq+f45kd/lnZaxVxQKA5Y+oI/LuTahr8BzSd4/6xet+8ufuM9k+tGqb4q25x/vXUxjTKCYbBXZWznKjYbmORuICPbVrRbKQuY9H2eAVY0Y8pC4YI+yNqjaIZSFqxR7OFWNETkLJikHDhUTI3IVzJELF7KjZY2KlogA24VEylpoNmNuXXOaGt2RSo/hLWDiL8JX2c/lZm4U/G0NpDrRIj4RSyJen7OEhUKCn/fnvgvx8TFmIcPkGuBRzI+g5Z2Wu1cWn4Nfhb0CFwoDnh3j0ub5ktr7HeMtWT8zGSOSychGycc3Mugqi6drLJKJqkAwCBk1AuFrgPAbCFejheK3jDK3613aUrLMqtTT5LDEvtxpXyn+nVVqtt5sozanSv1AIcf/AIrmNg9P2esIfgSrYuPsV70NRaTyHurcXyS1uw8y9OXUMh83pjLwt+7V/oacfYr3oai0kb3Rw98ktbsPcmiepdH5rTWXcO+NW+hpx9ivehqLSWvc7DnyT1noMkun+qlH5rTWWcP3Wr9DVOPcU56eotJbwZhvNPWegyC6z6ukfmtN5X8sUp9DVOPMU56ep2lvBWG809d6D2Lg/WWl81pnKf8AiT/Q1TjrE+enqdpTgjDOaeu9B7FxrraS+b0zlFg7P9oN9DVOOcS/16naU4Hwzmnr9hkkiOuwogCOlsnMPoD7HH6Gqcb4l/r1O0pwPhnNPX7DEnsM698mhn0G70llAMpFPy3HLGeWIlvew2IQRD1XrHvO92IXilKlJwSkrHZGx2PPltdlpPddz8Ou1WNWMZOUXarZNq1ZnZZyE0/h6aF6idVsX5c3xh1iOPykoZ8ES/EpHJjAkRIyp0wMPLzCXgA8eF61k2guO5T+Ty38XLb5aA9qALUB8sHdQBYO4KALB3BQBYO4KALB3BQBYO4KALB3BQBYO6gCwd1AfaAKAKAKAKAKAKAKAKAKAKAKAKAKAKA//9k='; } +function updateData($nameFile, $WAver, $WAToken = null) +{ + $file = __DIR__.'/'.$nameFile; + $open = fopen($file, 'r+'); + $content = fread($open, filesize($file)); + fclose($open); + + $content = explode("\n", $content); -//Generate Array of Emojis iOS2, iOS5 and iOS7 -function ArrayEmojis(){ - return array(array('iOS2' => '','iOS5' => '😄','iOS7' => '','Hex' => '1F604'), array('iOS2' => '','iOS5' => '😃','iOS7' => '','Hex' => '1F603'), array('iOS2' => '','iOS5' => '😀','iOS7' => '','Hex' => '1F600'), array('iOS2' => '','iOS5' => '😊','iOS7' => '','Hex' => '1F60A'), array('iOS2' => '','iOS5' => '☺','iOS7' => '☺️','Hex' => '263A'), array('iOS2' => '','iOS5' => '😉','iOS7' => '','Hex' => '1F609'), array('iOS2' => '','iOS5' => '😍','iOS7' => '','Hex' => '1F60D'), array('iOS2' => '','iOS5' => '😘','iOS7' => '','Hex' => '1F618'), array('iOS2' => '','iOS5' => '😚','iOS7' => '','Hex' => '1F61A'), array('iOS2' => '','iOS5' => '😗','iOS7' => '','Hex' => '1F617'), array('iOS2' => '','iOS5' => '😙','iOS7' => '','Hex' => '1F619'), array('iOS2' => '','iOS5' => '😜','iOS7' => '','Hex' => '1F61C'), array('iOS2' => '','iOS5' => '😝','iOS7' => '','Hex' => '1F61D'), array('iOS2' => '','iOS5' => '😛','iOS7' => '','Hex' => '1F61B'), array('iOS2' => '','iOS5' => '😳','iOS7' => '','Hex' => '1F633'), array('iOS2' => '','iOS5' => '😁','iOS7' => '','Hex' => '1F601'), array('iOS2' => '','iOS5' => '😔','iOS7' => '','Hex' => '1F614'), array('iOS2' => '','iOS5' => '😌','iOS7' => '','Hex' => '1F60C'), array('iOS2' => '','iOS5' => '😒','iOS7' => '','Hex' => '1F612'), array('iOS2' => '','iOS5' => '😞','iOS7' => '','Hex' => '1F61E'), array('iOS2' => '','iOS5' => '😣','iOS7' => '','Hex' => '1F623'), array('iOS2' => '','iOS5' => '😢','iOS7' => '','Hex' => '1F622'), array('iOS2' => '','iOS5' => '😂','iOS7' => '','Hex' => '1F602'), array('iOS2' => '','iOS5' => '😭','iOS7' => '','Hex' => '1F62D'), array('iOS2' => '','iOS5' => '😪','iOS7' => '','Hex' => '1F62A'), array('iOS2' => '','iOS5' => '😥','iOS7' => '','Hex' => '1F625'), array('iOS2' => '','iOS5' => '😰','iOS7' => '','Hex' => '1F630'), array('iOS2' => '','iOS5' => '😅','iOS7' => '','Hex' => '1F605'), array('iOS2' => '','iOS5' => '😓','iOS7' => '','Hex' => '1F613'), array('iOS2' => '','iOS5' => '😩','iOS7' => '','Hex' => '1F629'), array('iOS2' => '','iOS5' => '😫','iOS7' => '','Hex' => '1F62B'), array('iOS2' => '','iOS5' => '😨','iOS7' => '','Hex' => '1F628'), array('iOS2' => '','iOS5' => '😱','iOS7' => '','Hex' => '1F631'), array('iOS2' => '','iOS5' => '😠','iOS7' => '','Hex' => '1F620'), array('iOS2' => '','iOS5' => '😡','iOS7' => '','Hex' => '1F621'), array('iOS2' => '','iOS5' => '😤','iOS7' => '','Hex' => '1F624'), array('iOS2' => '','iOS5' => '😖','iOS7' => '','Hex' => '1F616'), array('iOS2' => '','iOS5' => '😆','iOS7' => '','Hex' => '1F606'), array('iOS2' => '','iOS5' => '😋','iOS7' => '','Hex' => '1F60B'), array('iOS2' => '','iOS5' => '😷','iOS7' => '','Hex' => '1F637'), array('iOS2' => '','iOS5' => '😎','iOS7' => '','Hex' => '1F60E'), array('iOS2' => '','iOS5' => '😴','iOS7' => '','Hex' => '1F634'), array('iOS2' => '','iOS5' => '😵','iOS7' => '','Hex' => '1F635'), array('iOS2' => '','iOS5' => '😲','iOS7' => '','Hex' => '1F632'), array('iOS2' => '','iOS5' => '😟','iOS7' => '','Hex' => '1F61F'), array('iOS2' => '','iOS5' => '😦','iOS7' => '','Hex' => '1F626'), array('iOS2' => '','iOS5' => '😧','iOS7' => '','Hex' => '1F627'), array('iOS2' => '','iOS5' => '😈','iOS7' => '','Hex' => '1F608'), array('iOS2' => '','iOS5' => '👿','iOS7' => '','Hex' => '1F47F'), array('iOS2' => '','iOS5' => '😮','iOS7' => '','Hex' => '1F62E'), array('iOS2' => '','iOS5' => '😬','iOS7' => '','Hex' => '1F62C'), array('iOS2' => '','iOS5' => '😐','iOS7' => '','Hex' => '1F610'), array('iOS2' => '','iOS5' => '😕','iOS7' => '','Hex' => '1F615'), array('iOS2' => '','iOS5' => '😯','iOS7' => '','Hex' => '1F62F'), array('iOS2' => '','iOS5' => '😶','iOS7' => '','Hex' => '1F636'), array('iOS2' => '','iOS5' => '😇','iOS7' => '','Hex' => '1F607'), array('iOS2' => '','iOS5' => '😏','iOS7' => '','Hex' => '1F60F'), array('iOS2' => '','iOS5' => '😑','iOS7' => '','Hex' => '1F611'), array('iOS2' => '','iOS5' => '👲','iOS7' => '','Hex' => '1F472'), array('iOS2' => '','iOS5' => '👳','iOS7' => '','Hex' => '1F473'), array('iOS2' => '','iOS5' => '👮','iOS7' => '','Hex' => '1F46E'), array('iOS2' => '','iOS5' => '👷','iOS7' => '','Hex' => '1F477'), array('iOS2' => '','iOS5' => '💂','iOS7' => '','Hex' => '1F482'), array('iOS2' => '','iOS5' => '👶','iOS7' => '','Hex' => '1F476'), array('iOS2' => '','iOS5' => '👦','iOS7' => '','Hex' => '1F466'), array('iOS2' => '','iOS5' => '👧','iOS7' => '','Hex' => '1F467'), array('iOS2' => '','iOS5' => '👨','iOS7' => '','Hex' => '1F468'), array('iOS2' => '','iOS5' => '👩','iOS7' => '','Hex' => '1F469'), array('iOS2' => '','iOS5' => '👴','iOS7' => '','Hex' => '1F474'), array('iOS2' => '','iOS5' => '👵','iOS7' => '','Hex' => '1F475'), array('iOS2' => '','iOS5' => '👱','iOS7' => '','Hex' => '1F471'), array('iOS2' => '','iOS5' => '👼','iOS7' => '','Hex' => '1F47C'), array('iOS2' => '','iOS5' => '👸','iOS7' => '','Hex' => '1F478'), array('iOS2' => '','iOS5' => '😺','iOS7' => '','Hex' => '1F63A'), array('iOS2' => '','iOS5' => '😸','iOS7' => '','Hex' => '1F638'), array('iOS2' => '','iOS5' => '😻','iOS7' => '','Hex' => '1F63B'), array('iOS2' => '','iOS5' => '😽','iOS7' => '','Hex' => '1F63D'), array('iOS2' => '','iOS5' => '😼','iOS7' => '','Hex' => '1F63C'), array('iOS2' => '','iOS5' => '🙀','iOS7' => '','Hex' => '1F640'), array('iOS2' => '','iOS5' => '😿','iOS7' => '','Hex' => '1F63F'), array('iOS2' => '','iOS5' => '😹','iOS7' => '','Hex' => '1F639'), array('iOS2' => '','iOS5' => '😾','iOS7' => '','Hex' => '1F63E'), array('iOS2' => '','iOS5' => '👹','iOS7' => '','Hex' => '1F479'), array('iOS2' => '','iOS5' => '👺','iOS7' => '','Hex' => '1F47A'), array('iOS2' => '','iOS5' => '🙈','iOS7' => '','Hex' => '1F648'), array('iOS2' => '','iOS5' => '🙉','iOS7' => '','Hex' => '1F649'), array('iOS2' => '','iOS5' => '🙊','iOS7' => '','Hex' => '1F64A'), array('iOS2' => '','iOS5' => '💀','iOS7' => '','Hex' => '1F480'), array('iOS2' => '','iOS5' => '👽','iOS7' => '','Hex' => '1F47D'), array('iOS2' => '','iOS5' => '💩','iOS7' => '','Hex' => '1F4A9'), array('iOS2' => '','iOS5' => '🔥','iOS7' => '','Hex' => '1F525'), array('iOS2' => '','iOS5' => '✨','iOS7' => '','Hex' => '2728'), array('iOS2' => '','iOS5' => '🌟','iOS7' => '','Hex' => '1F31F'), array('iOS2' => '','iOS5' => '💫','iOS7' => '','Hex' => '1F4AB'), array('iOS2' => '','iOS5' => '💥','iOS7' => '','Hex' => '1F4A5'), array('iOS2' => '','iOS5' => '💢','iOS7' => '','Hex' => '1F4A2'), array('iOS2' => '','iOS5' => '💦','iOS7' => '','Hex' => '1F4A6'), array('iOS2' => '','iOS5' => '💧','iOS7' => '','Hex' => '1F4A7'), array('iOS2' => '','iOS5' => '💤','iOS7' => '','Hex' => '1F4A4'), array('iOS2' => '','iOS5' => '💨','iOS7' => '','Hex' => '1F4A8'), array('iOS2' => '','iOS5' => '👂','iOS7' => '','Hex' => '1F442'), array('iOS2' => '','iOS5' => '👀','iOS7' => '','Hex' => '1F440'), array('iOS2' => '','iOS5' => '👃','iOS7' => '','Hex' => '1F443'), array('iOS2' => '','iOS5' => '👅','iOS7' => '','Hex' => '1F445'), array('iOS2' => '','iOS5' => '👄','iOS7' => '','Hex' => '1F444'), array('iOS2' => '','iOS5' => '👍','iOS7' => '','Hex' => '1F44D'), array('iOS2' => '','iOS5' => '👎','iOS7' => '','Hex' => '1F44E'), array('iOS2' => '','iOS5' => '👌','iOS7' => '','Hex' => '1F44C'), array('iOS2' => '','iOS5' => '👊','iOS7' => '','Hex' => '1F44A'), array('iOS2' => '','iOS5' => '✊','iOS7' => '','Hex' => '270A'), array('iOS2' => '','iOS5' => '✌','iOS7' => '✌️','Hex' => '270C'), array('iOS2' => '','iOS5' => '👋','iOS7' => '','Hex' => '1F44B'), array('iOS2' => '','iOS5' => '✋','iOS7' => '','Hex' => '270B'), array('iOS2' => '','iOS5' => '👐','iOS7' => '','Hex' => '1F450'), array('iOS2' => '','iOS5' => '👆','iOS7' => '','Hex' => '1F446'), array('iOS2' => '','iOS5' => '👇','iOS7' => '','Hex' => '1F447'), array('iOS2' => '','iOS5' => '👉','iOS7' => '','Hex' => '1F449'), array('iOS2' => '','iOS5' => '👈','iOS7' => '','Hex' => '1F448'), array('iOS2' => '','iOS5' => '🙌','iOS7' => '','Hex' => '1F64C'), array('iOS2' => '','iOS5' => '🙏','iOS7' => '','Hex' => '1F64F'), array('iOS2' => '','iOS5' => '☝','iOS7' => '☝️','Hex' => '261D'), array('iOS2' => '','iOS5' => '👏','iOS7' => '','Hex' => '1F44F'), array('iOS2' => '','iOS5' => '💪','iOS7' => '','Hex' => '1F4AA'), array('iOS2' => '','iOS5' => '🚶','iOS7' => '','Hex' => '1F6B6'), array('iOS2' => '','iOS5' => '🏃','iOS7' => '','Hex' => '1F3C3'), array('iOS2' => '','iOS5' => '💃','iOS7' => '','Hex' => '1F483'), array('iOS2' => '','iOS5' => '👫','iOS7' => '','Hex' => '1F46B'), array('iOS2' => '','iOS5' => '👪','iOS7' => '','Hex' => '1F46A'), array('iOS2' => '','iOS5' => '👬','iOS7' => '','Hex' => '1F46C'), array('iOS2' => '','iOS5' => '👭','iOS7' => '','Hex' => '1F46D'), array('iOS2' => '','iOS5' => '💏','iOS7' => '','Hex' => '1F48F'), array('iOS2' => '','iOS5' => '💑','iOS7' => '','Hex' => '1F491'), array('iOS2' => '','iOS5' => '👯','iOS7' => '','Hex' => '1F46F'), array('iOS2' => '','iOS5' => '🙆','iOS7' => '','Hex' => '1F646'), array('iOS2' => '','iOS5' => '🙅','iOS7' => '','Hex' => '1F645'), array('iOS2' => '','iOS5' => '💁','iOS7' => '','Hex' => '1F481'), array('iOS2' => '','iOS5' => '🙋','iOS7' => '','Hex' => '1F64B'), array('iOS2' => '','iOS5' => '💆','iOS7' => '','Hex' => '1F486'), array('iOS2' => '','iOS5' => '💇','iOS7' => '','Hex' => '1F487'), array('iOS2' => '','iOS5' => '💅','iOS7' => '','Hex' => '1F485'), array('iOS2' => '','iOS5' => '👰','iOS7' => '','Hex' => '1F470'), array('iOS2' => '','iOS5' => '🙎','iOS7' => '','Hex' => '1F64E'), array('iOS2' => '','iOS5' => '🙍','iOS7' => '','Hex' => '1F64D'), array('iOS2' => '','iOS5' => '🙇','iOS7' => '','Hex' => '1F647'), array('iOS2' => '','iOS5' => '🎩','iOS7' => '','Hex' => '1F3A9'), array('iOS2' => '','iOS5' => '👑','iOS7' => '','Hex' => '1F451'), array('iOS2' => '','iOS5' => '👒','iOS7' => '','Hex' => '1F452'), array('iOS2' => '','iOS5' => '👟','iOS7' => '','Hex' => '1F45F'), array('iOS2' => '','iOS5' => '👞','iOS7' => '','Hex' => '1F45E'), array('iOS2' => '','iOS5' => '👡','iOS7' => '','Hex' => '1F461'), array('iOS2' => '','iOS5' => '👠','iOS7' => '','Hex' => '1F460'), array('iOS2' => '','iOS5' => '👢','iOS7' => '','Hex' => '1F462'), array('iOS2' => '','iOS5' => '👕','iOS7' => '','Hex' => '1F455'), array('iOS2' => '','iOS5' => '👔','iOS7' => '','Hex' => '1F454'), array('iOS2' => '','iOS5' => '👚','iOS7' => '','Hex' => '1F45A'), array('iOS2' => '','iOS5' => '👗','iOS7' => '','Hex' => '1F457'), array('iOS2' => '','iOS5' => '🎽','iOS7' => '','Hex' => '1F3BD'), array('iOS2' => '','iOS5' => '👖','iOS7' => '','Hex' => '1F456'), array('iOS2' => '','iOS5' => '👘','iOS7' => '','Hex' => '1F458'), array('iOS2' => '','iOS5' => '👙','iOS7' => '','Hex' => '1F459'), array('iOS2' => '','iOS5' => '💼','iOS7' => '','Hex' => '1F4BC'), array('iOS2' => '','iOS5' => '👜','iOS7' => '','Hex' => '1F45C'), array('iOS2' => '','iOS5' => '👝','iOS7' => '','Hex' => '1F45D'), array('iOS2' => '','iOS5' => '👛','iOS7' => '','Hex' => '1F45B'), array('iOS2' => '','iOS5' => '👓','iOS7' => '','Hex' => '1F453'), array('iOS2' => '','iOS5' => '🎀','iOS7' => '','Hex' => '1F380'), array('iOS2' => '','iOS5' => '🌂','iOS7' => '','Hex' => '1F302'), array('iOS2' => '','iOS5' => '💄','iOS7' => '','Hex' => '1F484'), array('iOS2' => '','iOS5' => '💛','iOS7' => '','Hex' => '1F49B'), array('iOS2' => '','iOS5' => '💙','iOS7' => '','Hex' => '1F499'), array('iOS2' => '','iOS5' => '💜','iOS7' => '','Hex' => '1F49C'), array('iOS2' => '','iOS5' => '💚','iOS7' => '','Hex' => '1F49A'), array('iOS2' => '','iOS5' => '❤','iOS7' => '❤️','Hex' => '2764'), array('iOS2' => '','iOS5' => '💔','iOS7' => '','Hex' => '1F494'), array('iOS2' => '','iOS5' => '💗','iOS7' => '','Hex' => '1F497'), array('iOS2' => '','iOS5' => '💓','iOS7' => '','Hex' => '1F493'), array('iOS2' => '','iOS5' => '💕','iOS7' => '','Hex' => '1F495'), array('iOS2' => '','iOS5' => '💖','iOS7' => '','Hex' => '1F496'), array('iOS2' => '','iOS5' => '💞','iOS7' => '','Hex' => '1F49E'), array('iOS2' => '','iOS5' => '💘','iOS7' => '','Hex' => '1F498'), array('iOS2' => '','iOS5' => '💌','iOS7' => '','Hex' => '1F48C'), array('iOS2' => '','iOS5' => '💋','iOS7' => '','Hex' => '1F48B'), array('iOS2' => '','iOS5' => '💍','iOS7' => '','Hex' => '1F48D'), array('iOS2' => '','iOS5' => '💎','iOS7' => '','Hex' => '1F48E'), array('iOS2' => '','iOS5' => '👤','iOS7' => '','Hex' => '1F464'), array('iOS2' => '','iOS5' => '👥','iOS7' => '','Hex' => '1F465'), array('iOS2' => '','iOS5' => '💬','iOS7' => '','Hex' => '1F4AC'), array('iOS2' => '','iOS5' => '👣','iOS7' => '','Hex' => '1F463'), array('iOS2' => '','iOS5' => '💭','iOS7' => '','Hex' => '1F4AD'), array('iOS2' => '','iOS5' => '🏠','iOS7' => '','Hex' => '1F3E0'), array('iOS2' => '','iOS5' => '🏡','iOS7' => '','Hex' => '1F3E1'), array('iOS2' => '','iOS5' => '🏫','iOS7' => '','Hex' => '1F3EB'), array('iOS2' => '','iOS5' => '🏢','iOS7' => '','Hex' => '1F3E2'), array('iOS2' => '','iOS5' => '🏣','iOS7' => '','Hex' => '1F3E3'), array('iOS2' => '','iOS5' => '🏥','iOS7' => '','Hex' => '1F3E5'), array('iOS2' => '','iOS5' => '🏦','iOS7' => '','Hex' => '1F3E6'), array('iOS2' => '','iOS5' => '🏪','iOS7' => '','Hex' => '1F3EA'), array('iOS2' => '','iOS5' => '🏩','iOS7' => '','Hex' => '1F3E9'), array('iOS2' => '','iOS5' => '🏨','iOS7' => '','Hex' => '1F3E8'), array('iOS2' => '','iOS5' => '💒','iOS7' => '','Hex' => '1F492'), array('iOS2' => '','iOS5' => '⛪','iOS7' => '⛪️','Hex' => '26EA'), array('iOS2' => '','iOS5' => '🏬','iOS7' => '','Hex' => '1F3EC'), array('iOS2' => '','iOS5' => '🏤','iOS7' => '','Hex' => '1F3E4'), array('iOS2' => '','iOS5' => '🌇','iOS7' => '','Hex' => '1F307'), array('iOS2' => '','iOS5' => '🌆','iOS7' => '','Hex' => '1F306'), array('iOS2' => '','iOS5' => '🏯','iOS7' => '','Hex' => '1F3EF'), array('iOS2' => '','iOS5' => '🏰','iOS7' => '','Hex' => '1F3F0'), array('iOS2' => '','iOS5' => '⛺','iOS7' => '⛺️','Hex' => '26FA'), array('iOS2' => '','iOS5' => '🏭','iOS7' => '','Hex' => '1F3ED'), array('iOS2' => '','iOS5' => '🗼','iOS7' => '','Hex' => '1F5FC'), array('iOS2' => '','iOS5' => '🗾','iOS7' => '','Hex' => '1F5FE'), array('iOS2' => '','iOS5' => '🗻','iOS7' => '','Hex' => '1F5FB'), array('iOS2' => '','iOS5' => '🌄','iOS7' => '','Hex' => '1F304'), array('iOS2' => '','iOS5' => '🌅','iOS7' => '','Hex' => '1F305'), array('iOS2' => '','iOS5' => '🌃','iOS7' => '','Hex' => '1F303'), array('iOS2' => '','iOS5' => '🗽','iOS7' => '','Hex' => '1F5FD'), array('iOS2' => '','iOS5' => '🌉','iOS7' => '','Hex' => '1F309'), array('iOS2' => '','iOS5' => '🎠','iOS7' => '','Hex' => '1F3A0'), array('iOS2' => '','iOS5' => '🎡','iOS7' => '','Hex' => '1F3A1'), array('iOS2' => '','iOS5' => '⛲','iOS7' => '⛲️','Hex' => '26F2'), array('iOS2' => '','iOS5' => '🎢','iOS7' => '','Hex' => '1F3A2'), array('iOS2' => '','iOS5' => '🚢','iOS7' => '','Hex' => '1F6A2'), array('iOS2' => '','iOS5' => '⛵','iOS7' => '⛵️','Hex' => '26F5'), array('iOS2' => '','iOS5' => '🚤','iOS7' => '','Hex' => '1F6A4'), array('iOS2' => '','iOS5' => '🚣','iOS7' => '','Hex' => '1F6A3'), array('iOS2' => '','iOS5' => '⚓','iOS7' => '⚓️','Hex' => '2693'), array('iOS2' => '','iOS5' => '🚀','iOS7' => '','Hex' => '1F680'), array('iOS2' => '','iOS5' => '✈','iOS7' => '✈️','Hex' => '2708'), array('iOS2' => '','iOS5' => '💺','iOS7' => '','Hex' => '1F4BA'), array('iOS2' => '','iOS5' => '🚁','iOS7' => '','Hex' => '1F681'), array('iOS2' => '','iOS5' => '🚂','iOS7' => '','Hex' => '1F682'), array('iOS2' => '','iOS5' => '🚊','iOS7' => '','Hex' => '1F68A'), array('iOS2' => '','iOS5' => '🚉','iOS7' => '','Hex' => '1F689'), array('iOS2' => '','iOS5' => '🚞','iOS7' => '','Hex' => '1F69E'), array('iOS2' => '','iOS5' => '🚆','iOS7' => '','Hex' => '1F686'), array('iOS2' => '','iOS5' => '🚄','iOS7' => '','Hex' => '1F684'), array('iOS2' => '','iOS5' => '🚅','iOS7' => '','Hex' => '1F685'), array('iOS2' => '','iOS5' => '🚈','iOS7' => '','Hex' => '1F688'), array('iOS2' => '','iOS5' => '🚇','iOS7' => '','Hex' => '1F687'), array('iOS2' => '','iOS5' => '🚝','iOS7' => '','Hex' => '1F69D'), array('iOS2' => '','iOS5' => '🚋','iOS7' => '','Hex' => '1F68B'), array('iOS2' => '','iOS5' => '🚃','iOS7' => '','Hex' => '1F683'), array('iOS2' => '','iOS5' => '🚎','iOS7' => '','Hex' => '1F68E'), array('iOS2' => '','iOS5' => '🚌','iOS7' => '','Hex' => '1F68C'), array('iOS2' => '','iOS5' => '🚍','iOS7' => '','Hex' => '1F68D'), array('iOS2' => '','iOS5' => '🚙','iOS7' => '','Hex' => '1F699'), array('iOS2' => '','iOS5' => '🚘','iOS7' => '','Hex' => '1F698'), array('iOS2' => '','iOS5' => '🚗','iOS7' => '','Hex' => '1F697'), array('iOS2' => '','iOS5' => '🚕','iOS7' => '','Hex' => '1F695'), array('iOS2' => '','iOS5' => '🚖','iOS7' => '','Hex' => '1F696'), array('iOS2' => '','iOS5' => '🚛','iOS7' => '','Hex' => '1F69B'), array('iOS2' => '','iOS5' => '🚚','iOS7' => '','Hex' => '1F69A'), array('iOS2' => '','iOS5' => '🚨','iOS7' => '','Hex' => '1F6A8'), array('iOS2' => '','iOS5' => '🚓','iOS7' => '','Hex' => '1F693'), array('iOS2' => '','iOS5' => '🚔','iOS7' => '','Hex' => '1F694'), array('iOS2' => '','iOS5' => '🚒','iOS7' => '','Hex' => '1F692'), array('iOS2' => '','iOS5' => '🚑','iOS7' => '','Hex' => '1F691'), array('iOS2' => '','iOS5' => '🚐','iOS7' => '','Hex' => '1F690'), array('iOS2' => '','iOS5' => '🚲','iOS7' => '','Hex' => '1F6B2'), array('iOS2' => '','iOS5' => '🚡','iOS7' => '','Hex' => '1F6A1'), array('iOS2' => '','iOS5' => '🚟','iOS7' => '','Hex' => '1F69F'), array('iOS2' => '','iOS5' => '🚠','iOS7' => '','Hex' => '1F6A0'), array('iOS2' => '','iOS5' => '🚜','iOS7' => '','Hex' => '1F69C'), array('iOS2' => '','iOS5' => '💈','iOS7' => '','Hex' => '1F488'), array('iOS2' => '','iOS5' => '🚏','iOS7' => '','Hex' => '1F68F'), array('iOS2' => '','iOS5' => '🎫','iOS7' => '','Hex' => '1F3AB'), array('iOS2' => '','iOS5' => '🚦','iOS7' => '','Hex' => '1F6A6'), array('iOS2' => '','iOS5' => '🚥','iOS7' => '','Hex' => '1F6A5'), array('iOS2' => '','iOS5' => '⚠','iOS7' => '⚠️','Hex' => '26A0'), array('iOS2' => '','iOS5' => '🚧','iOS7' => '','Hex' => '1F6A7'), array('iOS2' => '','iOS5' => '🔰','iOS7' => '','Hex' => '1F530'), array('iOS2' => '','iOS5' => '⛽','iOS7' => '⛽️','Hex' => '26FD'), array('iOS2' => '','iOS5' => '🏮','iOS7' => '','Hex' => '1F3EE'), array('iOS2' => '','iOS5' => '🎰','iOS7' => '','Hex' => '1F3B0'), array('iOS2' => '','iOS5' => '♨','iOS7' => '♨️','Hex' => '2668'), array('iOS2' => '','iOS5' => '🗿','iOS7' => '','Hex' => '1F5FF'), array('iOS2' => '','iOS5' => '🎪','iOS7' => '','Hex' => '1F3AA'), array('iOS2' => '','iOS5' => '🎭','iOS7' => '','Hex' => '1F3AD'), array('iOS2' => '','iOS5' => '📍','iOS7' => '','Hex' => '1F4CD'), array('iOS2' => '','iOS5' => '🚩','iOS7' => '','Hex' => '1F6A9'), array('iOS2' => '','iOS5' => '🇯🇵','iOS7' => '','Hex' => '1F1EF_1F1F5'), array('iOS2' => '','iOS5' => '🇰🇷','iOS7' => '','Hex' => '1F1F0_1F1F7'), array('iOS2' => '','iOS5' => '🇩🇪','iOS7' => '','Hex' => '1F1E9_1F1EA'), array('iOS2' => '','iOS5' => '🇨🇳','iOS7' => '','Hex' => '1F1E8_1F1F3'), array('iOS2' => '','iOS5' => '🇺🇸','iOS7' => '','Hex' => '1F1FA_1F1F8'), array('iOS2' => '','iOS5' => '🇫🇷','iOS7' => '','Hex' => '1F1EB_1F1F7'), array('iOS2' => '','iOS5' => '🇪🇸','iOS7' => '','Hex' => '1F1EA_1F1F8'), array('iOS2' => '','iOS5' => '🇮🇹','iOS7' => '','Hex' => '1F1EE_1F1F9'), array('iOS2' => '','iOS5' => '🇷🇺','iOS7' => '','Hex' => '1F1F7_1F1FA'), array('iOS2' => '','iOS5' => '🇬🇧','iOS7' => '','Hex' => '1F1EC_1F1E7'), array('iOS2' => '','iOS5' => '','iOS7' => '','Hex' => ''), array('iOS2' => '','iOS5' => '🐶','iOS7' => '','Hex' => '1F436'), array('iOS2' => '','iOS5' => '🐺','iOS7' => '','Hex' => '1F43A'), array('iOS2' => '','iOS5' => '🐱','iOS7' => '','Hex' => '1F431'), array('iOS2' => '','iOS5' => '🐭','iOS7' => '','Hex' => '1F42D'), array('iOS2' => '','iOS5' => '🐹','iOS7' => '','Hex' => '1F439'), array('iOS2' => '','iOS5' => '🐰','iOS7' => '','Hex' => '1F430'), array('iOS2' => '','iOS5' => '🐸','iOS7' => '','Hex' => '1F438'), array('iOS2' => '','iOS5' => '🐯','iOS7' => '','Hex' => '1F42F'), array('iOS2' => '','iOS5' => '🐨','iOS7' => '','Hex' => '1F428'), array('iOS2' => '','iOS5' => '🐻','iOS7' => '','Hex' => '1F43B'), array('iOS2' => '','iOS5' => '🐷','iOS7' => '','Hex' => '1F437'), array('iOS2' => '','iOS5' => '🐽','iOS7' => '','Hex' => '1F43D'), array('iOS2' => '','iOS5' => '🐮','iOS7' => '','Hex' => '1F42E'), array('iOS2' => '','iOS5' => '🐗','iOS7' => '','Hex' => '1F417'), array('iOS2' => '','iOS5' => '🐵','iOS7' => '','Hex' => '1F435'), array('iOS2' => '','iOS5' => '🐒','iOS7' => '','Hex' => '1F412'), array('iOS2' => '','iOS5' => '🐴','iOS7' => '','Hex' => '1F434'), array('iOS2' => '','iOS5' => '🐑','iOS7' => '','Hex' => '1F411'), array('iOS2' => '','iOS5' => '🐘','iOS7' => '','Hex' => '1F418'), array('iOS2' => '','iOS5' => '🐼','iOS7' => '','Hex' => '1F43C'), array('iOS2' => '','iOS5' => '🐧','iOS7' => '','Hex' => '1F427'), array('iOS2' => '','iOS5' => '🐦','iOS7' => '','Hex' => '1F426'), array('iOS2' => '','iOS5' => '🐤','iOS7' => '','Hex' => '1F424'), array('iOS2' => '','iOS5' => '🐥','iOS7' => '','Hex' => '1F425'), array('iOS2' => '','iOS5' => '🐣','iOS7' => '','Hex' => '1F423'), array('iOS2' => '','iOS5' => '🐔','iOS7' => '','Hex' => '1F414'), array('iOS2' => '','iOS5' => '🐍','iOS7' => '','Hex' => '1F40D'), array('iOS2' => '','iOS5' => '🐢','iOS7' => '','Hex' => '1F422'), array('iOS2' => '','iOS5' => '🐛','iOS7' => '','Hex' => '1F41B'), array('iOS2' => '','iOS5' => '🐝','iOS7' => '','Hex' => '1F41D'), array('iOS2' => '','iOS5' => '🐜','iOS7' => '','Hex' => '1F41C'), array('iOS2' => '','iOS5' => '🐞','iOS7' => '','Hex' => '1F41E'), array('iOS2' => '','iOS5' => '🐌','iOS7' => '','Hex' => '1F40C'), array('iOS2' => '','iOS5' => '🐙','iOS7' => '','Hex' => '1F419'), array('iOS2' => '','iOS5' => '🐚','iOS7' => '','Hex' => '1F41A'), array('iOS2' => '','iOS5' => '🐠','iOS7' => '','Hex' => '1F420'), array('iOS2' => '','iOS5' => '🐟','iOS7' => '','Hex' => '1F41F'), array('iOS2' => '','iOS5' => '🐬','iOS7' => '','Hex' => '1F42C'), array('iOS2' => '','iOS5' => '🐳','iOS7' => '','Hex' => '1F433'), array('iOS2' => '','iOS5' => '🐋','iOS7' => '','Hex' => '1F40B'), array('iOS2' => '','iOS5' => '🐄','iOS7' => '','Hex' => '1F404'), array('iOS2' => '','iOS5' => '🐏','iOS7' => '','Hex' => '1F40F'), array('iOS2' => '','iOS5' => '🐀','iOS7' => '','Hex' => '1F400'), array('iOS2' => '','iOS5' => '🐃','iOS7' => '','Hex' => '1F403'), array('iOS2' => '','iOS5' => '🐅','iOS7' => '','Hex' => '1F405'), array('iOS2' => '','iOS5' => '🐇','iOS7' => '','Hex' => '1F407'), array('iOS2' => '','iOS5' => '🐉','iOS7' => '','Hex' => '1F409'), array('iOS2' => '','iOS5' => '🐎','iOS7' => '','Hex' => '1F40E'), array('iOS2' => '','iOS5' => '🐐','iOS7' => '','Hex' => '1F410'), array('iOS2' => '','iOS5' => '🐓','iOS7' => '','Hex' => '1F413'), array('iOS2' => '','iOS5' => '🐕','iOS7' => '','Hex' => '1F415'), array('iOS2' => '','iOS5' => '🐖','iOS7' => '','Hex' => '1F416'), array('iOS2' => '','iOS5' => '🐁','iOS7' => '','Hex' => '1F401'), array('iOS2' => '','iOS5' => '🐂','iOS7' => '','Hex' => '1F402'), array('iOS2' => '','iOS5' => '🐲','iOS7' => '','Hex' => '1F432'), array('iOS2' => '','iOS5' => '🐡','iOS7' => '','Hex' => '1F421'), array('iOS2' => '','iOS5' => '🐊','iOS7' => '','Hex' => '1F40A'), array('iOS2' => '','iOS5' => '🐫','iOS7' => '','Hex' => '1F42B'), array('iOS2' => '','iOS5' => '🐪','iOS7' => '','Hex' => '1F42A'), array('iOS2' => '','iOS5' => '🐆','iOS7' => '','Hex' => '1F406'), array('iOS2' => '','iOS5' => '🐈','iOS7' => '','Hex' => '1F408'), array('iOS2' => '','iOS5' => '🐩','iOS7' => '','Hex' => '1F429'), array('iOS2' => '','iOS5' => '🐾','iOS7' => '','Hex' => '1F43E'), array('iOS2' => '','iOS5' => '💐','iOS7' => '','Hex' => '1F490'), array('iOS2' => '','iOS5' => '🌸','iOS7' => '','Hex' => '1F338'), array('iOS2' => '','iOS5' => '🌷','iOS7' => '','Hex' => '1F337'), array('iOS2' => '','iOS5' => '🍀','iOS7' => '','Hex' => '1F340'), array('iOS2' => '','iOS5' => '🌹','iOS7' => '','Hex' => '1F339'), array('iOS2' => '','iOS5' => '🌻','iOS7' => '','Hex' => '1F33B'), array('iOS2' => '','iOS5' => '🌺','iOS7' => '','Hex' => '1F33A'), array('iOS2' => '','iOS5' => '🍁','iOS7' => '','Hex' => '1F341'), array('iOS2' => '','iOS5' => '🍃','iOS7' => '','Hex' => '1F343'), array('iOS2' => '','iOS5' => '🍂','iOS7' => '','Hex' => '1F342'), array('iOS2' => '','iOS5' => '🌿','iOS7' => '','Hex' => '1F33F'), array('iOS2' => '','iOS5' => '🌾','iOS7' => '','Hex' => '1F33E'), array('iOS2' => '','iOS5' => '🍄','iOS7' => '','Hex' => '1F344'), array('iOS2' => '','iOS5' => '🌵','iOS7' => '','Hex' => '1F335'), array('iOS2' => '','iOS5' => '🌴','iOS7' => '','Hex' => '1F334'), array('iOS2' => '','iOS5' => '🌲','iOS7' => '','Hex' => '1F332'), array('iOS2' => '','iOS5' => '🌳','iOS7' => '','Hex' => '1F333'), array('iOS2' => '','iOS5' => '🌰','iOS7' => '','Hex' => '1F330'), array('iOS2' => '','iOS5' => '🌱','iOS7' => '','Hex' => '1F331'), array('iOS2' => '','iOS5' => '🌼','iOS7' => '','Hex' => '1F33C'), array('iOS2' => '','iOS5' => '🌐','iOS7' => '','Hex' => '1F310'), array('iOS2' => '','iOS5' => '🌞','iOS7' => '','Hex' => '1F31E'), array('iOS2' => '','iOS5' => '🌝','iOS7' => '','Hex' => '1F31D'), array('iOS2' => '','iOS5' => '🌚','iOS7' => '','Hex' => '1F31A'), array('iOS2' => '','iOS5' => '🌑','iOS7' => '','Hex' => '1F311'), array('iOS2' => '','iOS5' => '🌒','iOS7' => '','Hex' => '1F312'), array('iOS2' => '','iOS5' => '🌓','iOS7' => '','Hex' => '1F313'), array('iOS2' => '','iOS5' => '🌔','iOS7' => '','Hex' => '1F314'), array('iOS2' => '','iOS5' => '🌕','iOS7' => '','Hex' => '1F315'), array('iOS2' => '','iOS5' => '🌖','iOS7' => '','Hex' => '1F316'), array('iOS2' => '','iOS5' => '🌗','iOS7' => '','Hex' => '1F317'), array('iOS2' => '','iOS5' => '🌘','iOS7' => '','Hex' => '1F318'), array('iOS2' => '','iOS5' => '🌜','iOS7' => '','Hex' => '1F31C'), array('iOS2' => '','iOS5' => '🌛','iOS7' => '','Hex' => '1F31B'), array('iOS2' => '','iOS5' => '🌙','iOS7' => '','Hex' => '1F319'), array('iOS2' => '','iOS5' => '🌍','iOS7' => '','Hex' => '1F30D'), array('iOS2' => '','iOS5' => '🌎','iOS7' => '','Hex' => '1F30E'), array('iOS2' => '','iOS5' => '🌏','iOS7' => '','Hex' => '1F30F'), array('iOS2' => '','iOS5' => '🌋','iOS7' => '','Hex' => '1F30B'), array('iOS2' => '','iOS5' => '🌌','iOS7' => '','Hex' => '1F30C'), array('iOS2' => '','iOS5' => '🌠','iOS7' => '','Hex' => '1F320'), array('iOS2' => '','iOS5' => '⭐','iOS7' => '⭐️','Hex' => '2B50'), array('iOS2' => '','iOS5' => '☀','iOS7' => '☀️','Hex' => '2600'), array('iOS2' => '','iOS5' => '⛅','iOS7' => '⛅️','Hex' => '26C5'), array('iOS2' => '','iOS5' => '☁','iOS7' => '☁️','Hex' => '2601'), array('iOS2' => '','iOS5' => '⚡','iOS7' => '⚡️','Hex' => '26A1'), array('iOS2' => '','iOS5' => '☔','iOS7' => '☔️','Hex' => '2614'), array('iOS2' => '','iOS5' => '❄','iOS7' => '❄️','Hex' => '2744'), array('iOS2' => '','iOS5' => '⛄','iOS7' => '⛄️','Hex' => '26C4'), array('iOS2' => '','iOS5' => '🌀','iOS7' => '🌀','Hex' => '1F300'), array('iOS2' => '','iOS5' => '🌁','iOS7' => '','Hex' => '1F301'), array('iOS2' => '','iOS5' => '🌈','iOS7' => '','Hex' => '1F308'), array('iOS2' => '','iOS5' => '🌊','iOS7' => '','Hex' => '1F30A'), array('iOS2' => '','iOS5' => '🎍','iOS7' => '','Hex' => '1F38D'), array('iOS2' => '','iOS5' => '💝','iOS7' => '','Hex' => '1F49D'), array('iOS2' => '','iOS5' => '🎎','iOS7' => '','Hex' => '1F38E'), array('iOS2' => '','iOS5' => '🎒','iOS7' => '','Hex' => '1F392'), array('iOS2' => '','iOS5' => '🎓','iOS7' => '','Hex' => '1F393'), array('iOS2' => '','iOS5' => '🎏','iOS7' => '','Hex' => '1F38F'), array('iOS2' => '','iOS5' => '🎆','iOS7' => '','Hex' => '1F386'), array('iOS2' => '','iOS5' => '🎇','iOS7' => '','Hex' => '1F387'), array('iOS2' => '','iOS5' => '🎐','iOS7' => '','Hex' => '1F390'), array('iOS2' => '','iOS5' => '🎑','iOS7' => '','Hex' => '1F391'), array('iOS2' => '','iOS5' => '🎃','iOS7' => '','Hex' => '1F383'), array('iOS2' => '','iOS5' => '👻','iOS7' => '','Hex' => '1F47B'), array('iOS2' => '','iOS5' => '🎅','iOS7' => '','Hex' => '1F385'), array('iOS2' => '','iOS5' => '🎄','iOS7' => '','Hex' => '1F384'), array('iOS2' => '','iOS5' => '🎁','iOS7' => '','Hex' => '1F381'), array('iOS2' => '','iOS5' => '🎋','iOS7' => '','Hex' => '1F38B'), array('iOS2' => '','iOS5' => '🎉','iOS7' => '','Hex' => '1F389'), array('iOS2' => '','iOS5' => '🎊','iOS7' => '','Hex' => '1F38A'), array('iOS2' => '','iOS5' => '🎈','iOS7' => '','Hex' => '1F388'), array('iOS2' => '','iOS5' => '🎌','iOS7' => '','Hex' => '1F38C'), array('iOS2' => '','iOS5' => '🔮','iOS7' => '','Hex' => '1F52E'), array('iOS2' => '','iOS5' => '🎥','iOS7' => '','Hex' => '1F3A5'), array('iOS2' => '','iOS5' => '📷','iOS7' => '','Hex' => '1F4F7'), array('iOS2' => '','iOS5' => '📹','iOS7' => '','Hex' => '1F4F9'), array('iOS2' => '','iOS5' => '📼','iOS7' => '','Hex' => '1F4FC'), array('iOS2' => '','iOS5' => '💿','iOS7' => '','Hex' => '1F4BF'), array('iOS2' => '','iOS5' => '📀','iOS7' => '','Hex' => '1F4C0'), array('iOS2' => '','iOS5' => '💽','iOS7' => '','Hex' => '1F4BD'), array('iOS2' => '','iOS5' => '💾','iOS7' => '','Hex' => '1F4BE'), array('iOS2' => '','iOS5' => '💻','iOS7' => '','Hex' => '1F4BB'), array('iOS2' => '','iOS5' => '📱','iOS7' => '','Hex' => '1F4F1'), array('iOS2' => '','iOS5' => '☎','iOS7' => '☎️','Hex' => '260E'), array('iOS2' => '','iOS5' => '📞','iOS7' => '','Hex' => '1F4DE'), array('iOS2' => '','iOS5' => '📟','iOS7' => '','Hex' => '1F4DF'), array('iOS2' => '','iOS5' => '📠','iOS7' => '','Hex' => '1F4E0'), array('iOS2' => '','iOS5' => '📡','iOS7' => '','Hex' => '1F4E1'), array('iOS2' => '','iOS5' => '📺','iOS7' => '','Hex' => '1F4FA'), array('iOS2' => '','iOS5' => '📻','iOS7' => '','Hex' => '1F4FB'), array('iOS2' => '','iOS5' => '🔊','iOS7' => '','Hex' => '1F50A'), array('iOS2' => '','iOS5' => '🔉','iOS7' => '','Hex' => '1F509'), array('iOS2' => '','iOS5' => '🔈','iOS7' => '','Hex' => '1F508'), array('iOS2' => '','iOS5' => '🔇','iOS7' => '','Hex' => '1F507'), array('iOS2' => '','iOS5' => '🔔','iOS7' => '','Hex' => '1F514'), array('iOS2' => '','iOS5' => '🔕','iOS7' => '','Hex' => '1F515'), array('iOS2' => '','iOS5' => '📢','iOS7' => '','Hex' => '1F4E2'), array('iOS2' => '','iOS5' => '📣','iOS7' => '','Hex' => '1F4E3'), array('iOS2' => '','iOS5' => '⏳','iOS7' => '','Hex' => '23F3'), array('iOS2' => '','iOS5' => '⌛','iOS7' => '⌛️','Hex' => '231B'), array('iOS2' => '','iOS5' => '⏰','iOS7' => '⏰','Hex' => '23F0'), array('iOS2' => '','iOS5' => '⌚','iOS7' => '⌚️','Hex' => '231A'), array('iOS2' => '','iOS5' => '🔓','iOS7' => '','Hex' => '1F513'), array('iOS2' => '','iOS5' => '🔒','iOS7' => '','Hex' => '1F512'), array('iOS2' => '','iOS5' => '🔏','iOS7' => '','Hex' => '1F50F'), array('iOS2' => '','iOS5' => '🔐','iOS7' => '','Hex' => '1F510'), array('iOS2' => '','iOS5' => '🔑','iOS7' => '','Hex' => '1F511'), array('iOS2' => '','iOS5' => '🔎','iOS7' => '','Hex' => '1F50E'), array('iOS2' => '','iOS5' => '💡','iOS7' => '','Hex' => '1F4A1'), array('iOS2' => '','iOS5' => '🔦','iOS7' => '','Hex' => '1F526'), array('iOS2' => '','iOS5' => '🔆','iOS7' => '','Hex' => '1F506'), array('iOS2' => '','iOS5' => '🔅','iOS7' => '','Hex' => '1F505'), array('iOS2' => '','iOS5' => '🔌','iOS7' => '','Hex' => '1F50C'), array('iOS2' => '','iOS5' => '🔋','iOS7' => '','Hex' => '1F50B'), array('iOS2' => '','iOS5' => '🔍','iOS7' => '','Hex' => '1F50D'), array('iOS2' => '','iOS5' => '🛁','iOS7' => '','Hex' => '1F6C1'), array('iOS2' => '','iOS5' => '🛀','iOS7' => '','Hex' => '1F6C0'), array('iOS2' => '','iOS5' => '🚿','iOS7' => '','Hex' => '1F6BF'), array('iOS2' => '','iOS5' => '🚽','iOS7' => '','Hex' => '1F6BD'), array('iOS2' => '','iOS5' => '🔧','iOS7' => '','Hex' => '1F527'), array('iOS2' => '','iOS5' => '🔩','iOS7' => '','Hex' => '1F529'), array('iOS2' => '','iOS5' => '🔨','iOS7' => '','Hex' => '1F528'), array('iOS2' => '','iOS5' => '🚪','iOS7' => '','Hex' => '1F6AA'), array('iOS2' => '','iOS5' => '🚬','iOS7' => '','Hex' => '1F6AC'), array('iOS2' => '','iOS5' => '💣','iOS7' => '','Hex' => '1F4A3'), array('iOS2' => '','iOS5' => '🔫','iOS7' => '','Hex' => '1F52B'), array('iOS2' => '','iOS5' => '🔪','iOS7' => '','Hex' => '1F52A'), array('iOS2' => '','iOS5' => '💊','iOS7' => '','Hex' => '1F48A'), array('iOS2' => '','iOS5' => '💉','iOS7' => '','Hex' => '1F489'), array('iOS2' => '','iOS5' => '💰','iOS7' => '','Hex' => '1F4B0'), array('iOS2' => '','iOS5' => '💴','iOS7' => '','Hex' => '1F4B4'), array('iOS2' => '','iOS5' => '💵','iOS7' => '','Hex' => '1F4B5'), array('iOS2' => '','iOS5' => '💷','iOS7' => '','Hex' => '1F4B7'), array('iOS2' => '','iOS5' => '💶','iOS7' => '','Hex' => '1F4B6'), array('iOS2' => '','iOS5' => '💳','iOS7' => '','Hex' => '1F4B3'), array('iOS2' => '','iOS5' => '💸','iOS7' => '','Hex' => '1F4B8'), array('iOS2' => '','iOS5' => '📲','iOS7' => '','Hex' => '1F4F2'), array('iOS2' => '','iOS5' => '📧','iOS7' => '','Hex' => '1F4E7'), array('iOS2' => '','iOS5' => '📥','iOS7' => '','Hex' => '1F4E5'), array('iOS2' => '','iOS5' => '📤','iOS7' => '','Hex' => '1F4E4'), array('iOS2' => '','iOS5' => '✉','iOS7' => '✉️','Hex' => '2709'), array('iOS2' => '','iOS5' => '📩','iOS7' => '','Hex' => '1F4E9'), array('iOS2' => '','iOS5' => '📨','iOS7' => '','Hex' => '1F4E8'), array('iOS2' => '','iOS5' => '📯','iOS7' => '','Hex' => '1F4EF'), array('iOS2' => '','iOS5' => '📫','iOS7' => '','Hex' => '1F4EB'), array('iOS2' => '','iOS5' => '📪','iOS7' => '','Hex' => '1F4EA'), array('iOS2' => '','iOS5' => '📬','iOS7' => '','Hex' => '1F4EC'), array('iOS2' => '','iOS5' => '📭','iOS7' => '','Hex' => '1F4ED'), array('iOS2' => '','iOS5' => '📮','iOS7' => '','Hex' => '1F4EE'), array('iOS2' => '','iOS5' => '📦','iOS7' => '','Hex' => '1F4E6'), array('iOS2' => '','iOS5' => '📝','iOS7' => '','Hex' => '1F4DD'), array('iOS2' => '','iOS5' => '📄','iOS7' => '','Hex' => '1F4C4'), array('iOS2' => '','iOS5' => '📃','iOS7' => '','Hex' => '1F4C3'), array('iOS2' => '','iOS5' => '📑','iOS7' => '','Hex' => '1F4D1'), array('iOS2' => '','iOS5' => '📊','iOS7' => '','Hex' => '1F4CA'), array('iOS2' => '','iOS5' => '📈','iOS7' => '','Hex' => '1F4C8'), array('iOS2' => '','iOS5' => '📉','iOS7' => '','Hex' => '1F4C9'), array('iOS2' => '','iOS5' => '📜','iOS7' => '','Hex' => '1F4DC'), array('iOS2' => '','iOS5' => '📋','iOS7' => '','Hex' => '1F4CB'), array('iOS2' => '','iOS5' => '📅','iOS7' => '','Hex' => '1F4C5'), array('iOS2' => '','iOS5' => '📆','iOS7' => '','Hex' => '1F4C6'), array('iOS2' => '','iOS5' => '📇','iOS7' => '','Hex' => '1F4C7'), array('iOS2' => '','iOS5' => '📁','iOS7' => '','Hex' => '1F4C1'), array('iOS2' => '','iOS5' => '📂','iOS7' => '','Hex' => '1F4C2'), array('iOS2' => '','iOS5' => '✂','iOS7' => '✂️','Hex' => '2702'), array('iOS2' => '','iOS5' => '📌','iOS7' => '','Hex' => '1F4CC'), array('iOS2' => '','iOS5' => '📎','iOS7' => '','Hex' => '1F4CE'), array('iOS2' => '','iOS5' => '✒','iOS7' => '✒️','Hex' => '2712'), array('iOS2' => '','iOS5' => '✏','iOS7' => '✏️','Hex' => '270F'), array('iOS2' => '','iOS5' => '📏','iOS7' => '','Hex' => '1F4CF'), array('iOS2' => '','iOS5' => '📐','iOS7' => '','Hex' => '1F4D0'), array('iOS2' => '','iOS5' => '📕','iOS7' => '','Hex' => '1F4D5'), array('iOS2' => '','iOS5' => '📗','iOS7' => '','Hex' => '1F4D7'), array('iOS2' => '','iOS5' => '📘','iOS7' => '','Hex' => '1F4D8'), array('iOS2' => '','iOS5' => '📙','iOS7' => '','Hex' => '1F4D9'), array('iOS2' => '','iOS5' => '📓','iOS7' => '','Hex' => '1F4D3'), array('iOS2' => '','iOS5' => '📔','iOS7' => '','Hex' => '1F4D4'), array('iOS2' => '','iOS5' => '📒','iOS7' => '','Hex' => '1F4D2'), array('iOS2' => '','iOS5' => '📚','iOS7' => '','Hex' => '1F4DA'), array('iOS2' => '','iOS5' => '📖','iOS7' => '','Hex' => '1F4D6'), array('iOS2' => '','iOS5' => '🔖','iOS7' => '','Hex' => '1F516'), array('iOS2' => '','iOS5' => '📛','iOS7' => '','Hex' => '1F4DB'), array('iOS2' => '','iOS5' => '🔬','iOS7' => '','Hex' => '1F52C'), array('iOS2' => '','iOS5' => '🔭','iOS7' => '','Hex' => '1F52D'), array('iOS2' => '','iOS5' => '📰','iOS7' => '','Hex' => '1F4F0'), array('iOS2' => '','iOS5' => '🎨','iOS7' => '','Hex' => '1F3A8'), array('iOS2' => '','iOS5' => '🎬','iOS7' => '','Hex' => '1F3AC'), array('iOS2' => '','iOS5' => '🎤','iOS7' => '','Hex' => '1F3A4'), array('iOS2' => '','iOS5' => '🎧','iOS7' => '','Hex' => '1F3A7'), array('iOS2' => '','iOS5' => '🎼','iOS7' => '','Hex' => '1F3BC'), array('iOS2' => '','iOS5' => '🎵','iOS7' => '','Hex' => '1F3B5'), array('iOS2' => '','iOS5' => '🎶','iOS7' => '','Hex' => '1F3B6'), array('iOS2' => '','iOS5' => '🎹','iOS7' => '','Hex' => '1F3B9'), array('iOS2' => '','iOS5' => '🎻','iOS7' => '','Hex' => '1F3BB'), array('iOS2' => '','iOS5' => '🎺','iOS7' => '','Hex' => '1F3BA'), array('iOS2' => '','iOS5' => '🎷','iOS7' => '','Hex' => '1F3B7'), array('iOS2' => '','iOS5' => '🎸','iOS7' => '','Hex' => '1F3B8'), array('iOS2' => '','iOS5' => '👾','iOS7' => '','Hex' => '1F47E'), array('iOS2' => '','iOS5' => '🎮','iOS7' => '','Hex' => '1F3AE'), array('iOS2' => '','iOS5' => '🃏','iOS7' => '','Hex' => '1F0CF'), array('iOS2' => '','iOS5' => '🎴','iOS7' => '','Hex' => '1F3B4'), array('iOS2' => '','iOS5' => '🀄','iOS7' => '🀄️','Hex' => '1F004'), array('iOS2' => '','iOS5' => '🎲','iOS7' => '','Hex' => '1F3B2'), array('iOS2' => '','iOS5' => '🎯','iOS7' => '','Hex' => '1F3AF'), array('iOS2' => '','iOS5' => '🏈','iOS7' => '','Hex' => '1F3C8'), array('iOS2' => '','iOS5' => '🏀','iOS7' => '','Hex' => '1F3C0'), array('iOS2' => '','iOS5' => '⚽','iOS7' => '⚽️','Hex' => '26BD'), array('iOS2' => '','iOS5' => '⚾','iOS7' => '⚾️','Hex' => '26BE'), array('iOS2' => '','iOS5' => '🎾','iOS7' => '','Hex' => '1F3BE'), array('iOS2' => '','iOS5' => '🎱','iOS7' => '','Hex' => '1F3B1'), array('iOS2' => '','iOS5' => '🏉','iOS7' => '','Hex' => '1F3C9'), array('iOS2' => '','iOS5' => '🎳','iOS7' => '','Hex' => '1F3B3'), array('iOS2' => '','iOS5' => '⛳','iOS7' => '⛳️','Hex' => '26F3'), array('iOS2' => '','iOS5' => '🚵','iOS7' => '','Hex' => '1F6B5'), array('iOS2' => '','iOS5' => '🚴','iOS7' => '','Hex' => '1F6B4'), array('iOS2' => '','iOS5' => '🏁','iOS7' => '','Hex' => '1F3C1'), array('iOS2' => '','iOS5' => '🏇','iOS7' => '','Hex' => '1F3C7'), array('iOS2' => '','iOS5' => '🏆','iOS7' => '','Hex' => '1F3C6'), array('iOS2' => '','iOS5' => '🎿','iOS7' => '','Hex' => '1F3BF'), array('iOS2' => '','iOS5' => '🏂','iOS7' => '','Hex' => '1F3C2'), array('iOS2' => '','iOS5' => '🏊','iOS7' => '','Hex' => '1F3CA'), array('iOS2' => '','iOS5' => '🏄','iOS7' => '','Hex' => '1F3C4'), array('iOS2' => '','iOS5' => '🎣','iOS7' => '','Hex' => '1F3A3'), array('iOS2' => '','iOS5' => '☕','iOS7' => '☕️','Hex' => '2615'), array('iOS2' => '','iOS5' => '🍵','iOS7' => '','Hex' => '1F375'), array('iOS2' => '','iOS5' => '🍶','iOS7' => '','Hex' => '1F376'), array('iOS2' => '','iOS5' => '🍼','iOS7' => '','Hex' => '1F37C'), array('iOS2' => '','iOS5' => '🍺','iOS7' => '','Hex' => '1F37A'), array('iOS2' => '','iOS5' => '🍻','iOS7' => '','Hex' => '1F37B'), array('iOS2' => '','iOS5' => '🍸','iOS7' => '','Hex' => '1F378'), array('iOS2' => '','iOS5' => '🍹','iOS7' => '','Hex' => '1F379'), array('iOS2' => '','iOS5' => '🍷','iOS7' => '','Hex' => '1F377'), array('iOS2' => '','iOS5' => '🍴','iOS7' => '','Hex' => '1F374'), array('iOS2' => '','iOS5' => '🍕','iOS7' => '','Hex' => '1F355'), array('iOS2' => '','iOS5' => '🍔','iOS7' => '','Hex' => '1F354'), array('iOS2' => '','iOS5' => '🍟','iOS7' => '','Hex' => '1F35F'), array('iOS2' => '','iOS5' => '🍗','iOS7' => '','Hex' => '1F357'), array('iOS2' => '','iOS5' => '🍖','iOS7' => '','Hex' => '1F356'), array('iOS2' => '','iOS5' => '🍝','iOS7' => '','Hex' => '1F35D'), array('iOS2' => '','iOS5' => '🍛','iOS7' => '','Hex' => '1F35B'), array('iOS2' => '','iOS5' => '🍤','iOS7' => '','Hex' => '1F364'), array('iOS2' => '','iOS5' => '🍱','iOS7' => '','Hex' => '1F371'), array('iOS2' => '','iOS5' => '🍣','iOS7' => '','Hex' => '1F363'), array('iOS2' => '','iOS5' => '🍥','iOS7' => '','Hex' => '1F365'), array('iOS2' => '','iOS5' => '🍙','iOS7' => '','Hex' => '1F359'), array('iOS2' => '','iOS5' => '🍘','iOS7' => '','Hex' => '1F358'), array('iOS2' => '','iOS5' => '🍚','iOS7' => '','Hex' => '1F35A'), array('iOS2' => '','iOS5' => '🍜','iOS7' => '','Hex' => '1F35C'), array('iOS2' => '','iOS5' => '🍲','iOS7' => '','Hex' => '1F372'), array('iOS2' => '','iOS5' => '🍢','iOS7' => '','Hex' => '1F362'), array('iOS2' => '','iOS5' => '🍡','iOS7' => '','Hex' => '1F361'), array('iOS2' => '','iOS5' => '🍳','iOS7' => '','Hex' => '1F373'), array('iOS2' => '','iOS5' => '🍞','iOS7' => '','Hex' => '1F35E'), array('iOS2' => '','iOS5' => '🍩','iOS7' => '','Hex' => '1F369'), array('iOS2' => '','iOS5' => '🍮','iOS7' => '','Hex' => '1F36E'), array('iOS2' => '','iOS5' => '🍦','iOS7' => '','Hex' => '1F366'), array('iOS2' => '','iOS5' => '🍨','iOS7' => '','Hex' => '1F368'), array('iOS2' => '','iOS5' => '🍧','iOS7' => '','Hex' => '1F367'), array('iOS2' => '','iOS5' => '🎂','iOS7' => '','Hex' => '1F382'), array('iOS2' => '','iOS5' => '🍰','iOS7' => '','Hex' => '1F370'), array('iOS2' => '','iOS5' => '🍪','iOS7' => '','Hex' => '1F36A'), array('iOS2' => '','iOS5' => '🍫','iOS7' => '','Hex' => '1F36B'), array('iOS2' => '','iOS5' => '🍬','iOS7' => '','Hex' => '1F36C'), array('iOS2' => '','iOS5' => '🍭','iOS7' => '','Hex' => '1F36D'), array('iOS2' => '','iOS5' => '🍯','iOS7' => '','Hex' => '1F36F'), array('iOS2' => '','iOS5' => '🍎','iOS7' => '','Hex' => '1F34E'), array('iOS2' => '','iOS5' => '🍏','iOS7' => '','Hex' => '1F34F'), array('iOS2' => '','iOS5' => '🍊','iOS7' => '','Hex' => '1F34A'), array('iOS2' => '','iOS5' => '🍋','iOS7' => '','Hex' => '1F34B'), array('iOS2' => '','iOS5' => '🍒','iOS7' => '','Hex' => '1F352'), array('iOS2' => '','iOS5' => '🍇','iOS7' => '','Hex' => '1F347'), array('iOS2' => '','iOS5' => '🍉','iOS7' => '','Hex' => '1F349'), array('iOS2' => '','iOS5' => '🍓','iOS7' => '','Hex' => '1F353'), array('iOS2' => '','iOS5' => '🍑','iOS7' => '','Hex' => '1F351'), array('iOS2' => '','iOS5' => '🍈','iOS7' => '','Hex' => '1F348'), array('iOS2' => '','iOS5' => '🍌','iOS7' => '','Hex' => '1F34C'), array('iOS2' => '','iOS5' => '🍐','iOS7' => '','Hex' => '1F350'), array('iOS2' => '','iOS5' => '🍍','iOS7' => '','Hex' => '1F34D'), array('iOS2' => '','iOS5' => '🍠','iOS7' => '','Hex' => '1F360'), array('iOS2' => '','iOS5' => '🍆','iOS7' => '','Hex' => '1F346'), array('iOS2' => '','iOS5' => '🍅','iOS7' => '','Hex' => '1F345'), array('iOS2' => '','iOS5' => '🌽','iOS7' => '','Hex' => '1F33D'), array('iOS2' => '','iOS5' => '1⃣','iOS7' => '','Hex' => '0031_20E3'), array('iOS2' => '','iOS5' => '2⃣','iOS7' => '','Hex' => '0032_20E3'), array('iOS2' => '','iOS5' => '3⃣','iOS7' => '','Hex' => '0033_20E3'), array('iOS2' => '','iOS5' => '4⃣','iOS7' => '','Hex' => '0034_20E3'), array('iOS2' => '','iOS5' => '2⃣','iOS7' => '','Hex' => '0032_20E3'), array('iOS2' => '','iOS5' => '0⃣','iOS7' => '','Hex' => '0030_20E3'), array('iOS2' => '','iOS5' => '5⃣','iOS7' => '','Hex' => '0035_20E3'), array('iOS2' => '','iOS5' => '6⃣','iOS7' => '','Hex' => '0036_20E3'), array('iOS2' => '','iOS5' => '7⃣','iOS7' => '','Hex' => '0037_20E3'), array('iOS2' => '','iOS5' => '8⃣','iOS7' => '','Hex' => '0038_20E3'), array('iOS2' => '','iOS5' => '9⃣','iOS7' => '','Hex' => '0039_20E3'), array('iOS2' => '','iOS5' => '🔟','iOS7' => '','Hex' => '1F51F'), array('iOS2' => '','iOS5' => '🔢','iOS7' => '','Hex' => '1F522'), array('iOS2' => '','iOS5' => '#⃣','iOS7' => '','Hex' => '0023_20E3'), array('iOS2' => '','iOS5' => '🔣','iOS7' => '','Hex' => '1F523'), array('iOS2' => '','iOS5' => '⬆','iOS7' => '⬆️','Hex' => '2B06'), array('iOS2' => '','iOS5' => '⬇','iOS7' => '⬇️','Hex' => '2B07'), array('iOS2' => '','iOS5' => '⬅','iOS7' => '⬅️','Hex' => '2B05'), array('iOS2' => '','iOS5' => '➡','iOS7' => '➡️','Hex' => '27A1'), array('iOS2' => '','iOS5' => '🔠','iOS7' => '','Hex' => '1F520'), array('iOS2' => '','iOS5' => '🔡','iOS7' => '','Hex' => '1F521'), array('iOS2' => '','iOS5' => '🔤','iOS7' => '','Hex' => '1F524'), array('iOS2' => '','iOS5' => '↗','iOS7' => '↗️','Hex' => '2197'), array('iOS2' => '','iOS5' => '↖','iOS7' => '↖️','Hex' => '2196'), array('iOS2' => '','iOS5' => '↘','iOS7' => '↘️','Hex' => '2198'), array('iOS2' => '','iOS5' => '↙','iOS7' => '↙️','Hex' => '2199'), array('iOS2' => '','iOS5' => '↔','iOS7' => '↔️','Hex' => '2194'), array('iOS2' => '','iOS5' => '↕','iOS7' => '↕️','Hex' => '2195'), array('iOS2' => '','iOS5' => '🔄','iOS7' => '','Hex' => '1F504'), array('iOS2' => '','iOS5' => '◀','iOS7' => '◀️','Hex' => '25C0'), array('iOS2' => '','iOS5' => '▶','iOS7' => '▶️','Hex' => '25B6'), array('iOS2' => '','iOS5' => '🔼','iOS7' => '','Hex' => '1F53C'), array('iOS2' => '','iOS5' => '🔽','iOS7' => '','Hex' => '1F53D'), array('iOS2' => '','iOS5' => '↩','iOS7' => '↩️','Hex' => '21A9'), array('iOS2' => '','iOS5' => '↪','iOS7' => '↪️','Hex' => '21AA'), array('iOS2' => '','iOS5' => 'ℹ','iOS7' => 'ℹ️','Hex' => '2139'), array('iOS2' => '','iOS5' => '⏪','iOS7' => '','Hex' => '23EA'), array('iOS2' => '','iOS5' => '⏩','iOS7' => '','Hex' => '23E9'), array('iOS2' => '','iOS5' => '⏫','iOS7' => '','Hex' => '23EB'), array('iOS2' => '','iOS5' => '⏬','iOS7' => '','Hex' => '23EC'), array('iOS2' => '','iOS5' => '⤵','iOS7' => '⤵️','Hex' => '2935'), array('iOS2' => '','iOS5' => '⤴','iOS7' => '⤴️','Hex' => '2934'), array('iOS2' => '','iOS5' => '🆗','iOS7' => '','Hex' => '1F197'), array('iOS2' => '','iOS5' => '🔀','iOS7' => '','Hex' => '1F500'), array('iOS2' => '','iOS5' => '🔁','iOS7' => '','Hex' => '1F501'), array('iOS2' => '','iOS5' => '🔂','iOS7' => '','Hex' => '1F502'), array('iOS2' => '','iOS5' => '🆕','iOS7' => '','Hex' => '1F195'), array('iOS2' => '','iOS5' => '🆙','iOS7' => '','Hex' => '1F199'), array('iOS2' => '','iOS5' => '🆒','iOS7' => '','Hex' => '1F192'), array('iOS2' => '','iOS5' => '🆓','iOS7' => '','Hex' => '1F193'), array('iOS2' => '','iOS5' => '🆖','iOS7' => '','Hex' => '1F196'), array('iOS2' => '','iOS5' => '📶','iOS7' => '','Hex' => '1F4F6'), array('iOS2' => '','iOS5' => '🎦','iOS7' => '','Hex' => '1F3A6'), array('iOS2' => '','iOS5' => '🈁','iOS7' => '','Hex' => '1F201'), array('iOS2' => '','iOS5' => '🈯','iOS7' => '🈯️','Hex' => '1F22F'), array('iOS2' => '','iOS5' => '🈳','iOS7' => '','Hex' => '1F233'), array('iOS2' => '','iOS5' => '🈵','iOS7' => '','Hex' => '1F235'), array('iOS2' => '','iOS5' => '🈴','iOS7' => '','Hex' => '1F234'), array('iOS2' => '','iOS5' => '🈲','iOS7' => '','Hex' => '1F232'), array('iOS2' => '','iOS5' => '🉐','iOS7' => '','Hex' => '1F250'), array('iOS2' => '','iOS5' => '🈹','iOS7' => '','Hex' => '1F239'), array('iOS2' => '','iOS5' => '🈺','iOS7' => '','Hex' => '1F23A'), array('iOS2' => '','iOS5' => '🈶','iOS7' => '','Hex' => '1F236'), array('iOS2' => '','iOS5' => '🈚','iOS7' => '🈚️','Hex' => '1F21A'), array('iOS2' => '','iOS5' => '🚻','iOS7' => '','Hex' => '1F6BB'), array('iOS2' => '','iOS5' => '🚹','iOS7' => '','Hex' => '1F6B9'), array('iOS2' => '','iOS5' => '🚺','iOS7' => '','Hex' => '1F6BA'), array('iOS2' => '','iOS5' => '🚼','iOS7' => '','Hex' => '1F6BC'), array('iOS2' => '','iOS5' => '🚾','iOS7' => '','Hex' => '1F6BE'), array('iOS2' => '','iOS5' => '🚰','iOS7' => '','Hex' => '1F6B0'), array('iOS2' => '','iOS5' => '🚮','iOS7' => '','Hex' => '1F6AE'), array('iOS2' => '','iOS5' => '🅿','iOS7' => '🅿️','Hex' => '1F17F'), array('iOS2' => '','iOS5' => '♿','iOS7' => '♿️','Hex' => '267F'), array('iOS2' => '','iOS5' => '🚭','iOS7' => '','Hex' => '1F6AD'), array('iOS2' => '','iOS5' => '🈷','iOS7' => '','Hex' => '1F237'), array('iOS2' => '','iOS5' => '🈸','iOS7' => '','Hex' => '1F238'), array('iOS2' => '','iOS5' => '🈂','iOS7' => '','Hex' => '1F202'), array('iOS2' => '','iOS5' => 'Ⓜ','iOS7' => 'Ⓜ️','Hex' => '24C2'), array('iOS2' => '','iOS5' => '🛂','iOS7' => '','Hex' => '1F6C2'), array('iOS2' => '','iOS5' => '🛄','iOS7' => '','Hex' => '1F6C4'), array('iOS2' => '','iOS5' => '🛅','iOS7' => '','Hex' => '1F6C5'), array('iOS2' => '','iOS5' => '🛃','iOS7' => '','Hex' => '1F6C3'), array('iOS2' => '','iOS5' => '🉑','iOS7' => '','Hex' => '1F251'), array('iOS2' => '','iOS5' => '㊙','iOS7' => '㊙️','Hex' => '3299'), array('iOS2' => '','iOS5' => '㊗','iOS7' => '㊗️','Hex' => '3297'), array('iOS2' => '','iOS5' => '🆑','iOS7' => '','Hex' => '1F191'), array('iOS2' => '','iOS5' => '🆘','iOS7' => '','Hex' => '1F198'), array('iOS2' => '','iOS5' => '🆔','iOS7' => '','Hex' => '1F194'), array('iOS2' => '','iOS5' => '🚫','iOS7' => '','Hex' => '1F6AB'), array('iOS2' => '','iOS5' => '🔞','iOS7' => '','Hex' => '1F51E'), array('iOS2' => '','iOS5' => '📵','iOS7' => '','Hex' => '1F4F5'), array('iOS2' => '','iOS5' => '🚯','iOS7' => '','Hex' => '1F6AF'), array('iOS2' => '','iOS5' => '🚱','iOS7' => '','Hex' => '1F6B1'), array('iOS2' => '','iOS5' => '🚳','iOS7' => '','Hex' => '1F6B3'), array('iOS2' => '','iOS5' => '🚷','iOS7' => '','Hex' => '1F6B7'), array('iOS2' => '','iOS5' => '🚸','iOS7' => '','Hex' => '1F6B8'), array('iOS2' => '','iOS5' => '⛔','iOS7' => '⛔️','Hex' => '26D4'), array('iOS2' => '','iOS5' => '✳','iOS7' => '✳️','Hex' => '2733'), array('iOS2' => '','iOS5' => '❇','iOS7' => '❇️','Hex' => '2747'), array('iOS2' => '','iOS5' => '❎','iOS7' => '','Hex' => '274E'), array('iOS2' => '','iOS5' => '✅','iOS7' => '','Hex' => '2705'), array('iOS2' => '','iOS5' => '✴','iOS7' => '✴️','Hex' => '2734'), array('iOS2' => '','iOS5' => '💟','iOS7' => '','Hex' => '1F49F'), array('iOS2' => '','iOS5' => '🆚','iOS7' => '','Hex' => '1F19A'), array('iOS2' => '','iOS5' => '📳','iOS7' => '','Hex' => '1F4F3'), array('iOS2' => '','iOS5' => '📴','iOS7' => '','Hex' => '1F4F4'), array('iOS2' => '','iOS5' => '🅰','iOS7' => '','Hex' => '1F170'), array('iOS2' => '','iOS5' => '🅱','iOS7' => '','Hex' => '1F171'), array('iOS2' => '','iOS5' => '🆎','iOS7' => '','Hex' => '1F18E'), array('iOS2' => '','iOS5' => '🅾','iOS7' => '','Hex' => '1F17E'), array('iOS2' => '','iOS5' => '💠','iOS7' => '','Hex' => '1F4A0'), array('iOS2' => '','iOS5' => '➿','iOS7' => '','Hex' => '27BF'), array('iOS2' => '','iOS5' => '♻','iOS7' => '♻️','Hex' => '267B'), array('iOS2' => '','iOS5' => '♈','iOS7' => '♈️','Hex' => '2648'), array('iOS2' => '','iOS5' => '♉','iOS7' => '♉️','Hex' => '2649'), array('iOS2' => '','iOS5' => '♊','iOS7' => '♊️','Hex' => '264A'), array('iOS2' => '','iOS5' => '♋','iOS7' => '♋️','Hex' => '264B'), array('iOS2' => '','iOS5' => '♌','iOS7' => '♌️','Hex' => '264C'), array('iOS2' => '','iOS5' => '♍','iOS7' => '♍️','Hex' => '264D'), array('iOS2' => '','iOS5' => '♎','iOS7' => '♎️','Hex' => '264E'), array('iOS2' => '','iOS5' => '♏','iOS7' => '♏️','Hex' => '264F'), array('iOS2' => '','iOS5' => '♐','iOS7' => '♐️','Hex' => '2650'), array('iOS2' => '','iOS5' => '♑','iOS7' => '♑️','Hex' => '2651'), array('iOS2' => '','iOS5' => '♒','iOS7' => '♒️','Hex' => '2652'), array('iOS2' => '','iOS5' => '♓','iOS7' => '♓️','Hex' => '2653'), array('iOS2' => '','iOS5' => '⛎','iOS7' => '','Hex' => '26CE'), array('iOS2' => '','iOS5' => '🔯','iOS7' => '','Hex' => '1F52F'), array('iOS2' => '','iOS5' => '🏧','iOS7' => '','Hex' => '1F3E7'), array('iOS2' => '','iOS5' => '💹','iOS7' => '','Hex' => '1F4B9'), array('iOS2' => '','iOS5' => '💲','iOS7' => '','Hex' => '1F4B2'), array('iOS2' => '','iOS5' => '💱','iOS7' => '','Hex' => '1F4B1'), array('iOS2' => '©','iOS5' => '©','iOS7' => '','Hex' => '00A9'), array('iOS2' => '®','iOS5' => '®','iOS7' => '','Hex' => '00AE'), array('iOS2' => '™','iOS5' => '™','iOS7' => '','Hex' => '2122'), array('iOS2' => '','iOS5' => '〽','iOS7' => '〽️','Hex' => '303D'), array('iOS2' => '','iOS5' => '〰','iOS7' => '','Hex' => '3030'), array('iOS2' => '','iOS5' => '🔝','iOS7' => '','Hex' => '1F51D'), array('iOS2' => '','iOS5' => '🔚','iOS7' => '','Hex' => '1F51A'), array('iOS2' => '','iOS5' => '🔙','iOS7' => '','Hex' => '1F519'), array('iOS2' => '','iOS5' => '🔛','iOS7' => '','Hex' => '1F51B'), array('iOS2' => '','iOS5' => '🔜','iOS7' => '','Hex' => '1F51C'), array('iOS2' => '','iOS5' => '❌','iOS7' => '','Hex' => '274C'), array('iOS2' => '','iOS5' => '⭕','iOS7' => '⭕️','Hex' => '2B55'), array('iOS2' => '','iOS5' => '❗','iOS7' => '❗️','Hex' => '2757'), array('iOS2' => '','iOS5' => '❓','iOS7' => '','Hex' => '2753'), array('iOS2' => '','iOS5' => '❕','iOS7' => '','Hex' => '2755'), array('iOS2' => '','iOS5' => '❔','iOS7' => '','Hex' => '2754'), array('iOS2' => '','iOS5' => '🔃','iOS7' => '','Hex' => '1F503'), array('iOS2' => '','iOS5' => '🕛','iOS7' => '','Hex' => '1F55B'), array('iOS2' => '','iOS5' => '🕧','iOS7' => '','Hex' => '1F567'), array('iOS2' => '','iOS5' => '🕐','iOS7' => '','Hex' => '1F550'), array('iOS2' => '','iOS5' => '🕜','iOS7' => '','Hex' => '1F55C'), array('iOS2' => '','iOS5' => '🕑','iOS7' => '','Hex' => '1F551'), array('iOS2' => '','iOS5' => '🕝','iOS7' => '','Hex' => '1F55D'), array('iOS2' => '','iOS5' => '🕒','iOS7' => '','Hex' => '1F552'), array('iOS2' => '','iOS5' => '🕞','iOS7' => '','Hex' => '1F55E'), array('iOS2' => '','iOS5' => '🕓','iOS7' => '','Hex' => '1F553'), array('iOS2' => '','iOS5' => '🕟','iOS7' => '','Hex' => '1F55F'), array('iOS2' => '','iOS5' => '🕔','iOS7' => '','Hex' => '1F554'), array('iOS2' => '','iOS5' => '🕠','iOS7' => '','Hex' => '1F560'), array('iOS2' => '','iOS5' => '🕕','iOS7' => '','Hex' => '1F555'), array('iOS2' => '','iOS5' => '🕖','iOS7' => '','Hex' => '1F556'), array('iOS2' => '','iOS5' => '🕗','iOS7' => '','Hex' => '1F557'), array('iOS2' => '','iOS5' => '🕘','iOS7' => '','Hex' => '1F558'), array('iOS2' => '','iOS5' => '🕙','iOS7' => '','Hex' => '1F559'), array('iOS2' => '','iOS5' => '🕚','iOS7' => '','Hex' => '1F55A'), array('iOS2' => '','iOS5' => '🕡','iOS7' => '','Hex' => '1F561'), array('iOS2' => '','iOS5' => '🕢','iOS7' => '','Hex' => '1F562'), array('iOS2' => '','iOS5' => '🕣','iOS7' => '','Hex' => '1F563'), array('iOS2' => '','iOS5' => '🕤','iOS7' => '','Hex' => '1F564'), array('iOS2' => '','iOS5' => '🕥','iOS7' => '','Hex' => '1F565'), array('iOS2' => '','iOS5' => '🕦','iOS7' => '','Hex' => '1F566'), array('iOS2' => '','iOS5' => '✖','iOS7' => '✖️','Hex' => '2716'), array('iOS2' => '','iOS5' => '➕','iOS7' => '','Hex' => '2795'), array('iOS2' => '','iOS5' => '➖','iOS7' => '','Hex' => '2796'), array('iOS2' => '','iOS5' => '➗','iOS7' => '','Hex' => '2797'), array('iOS2' => '','iOS5' => '♠','iOS7' => '♠️','Hex' => '2660'), array('iOS2' => '','iOS5' => '♥','iOS7' => '♥️','Hex' => '2665'), array('iOS2' => '','iOS5' => '♣','iOS7' => '♣️','Hex' => '2663'), array('iOS2' => '','iOS5' => '♦','iOS7' => '♦️','Hex' => '2666'), array('iOS2' => '','iOS5' => '💮','iOS7' => '','Hex' => '1F4AE'), array('iOS2' => '','iOS5' => '💯','iOS7' => '','Hex' => '1F4AF'), array('iOS2' => '','iOS5' => '✔','iOS7' => '✔️','Hex' => '2714'), array('iOS2' => '','iOS5' => '☑','iOS7' => '☑️','Hex' => '2611'), array('iOS2' => '','iOS5' => '🔘','iOS7' => '','Hex' => '1F518'), array('iOS2' => '','iOS5' => '🔗','iOS7' => '','Hex' => '1F517'), array('iOS2' => '','iOS5' => '➰','iOS7' => '','Hex' => '27B0'), array('iOS2' => '','iOS5' => '🔱','iOS7' => '','Hex' => '1F531'), array('iOS2' => '','iOS5' => '🔲','iOS7' => '','Hex' => '1F532'), array('iOS2' => '','iOS5' => '🔳','iOS7' => '','Hex' => '1F533'), array('iOS2' => '','iOS5' => '◼','iOS7' => '◼️','Hex' => '25FC'), array('iOS2' => '','iOS5' => '◻','iOS7' => '◻️','Hex' => '25FB'), array('iOS2' => '','iOS5' => '◾','iOS7' => '◾️','Hex' => '25FE'), array('iOS2' => '','iOS5' => '◽','iOS7' => '◽️','Hex' => '25FD'), array('iOS2' => '','iOS5' => '▪','iOS7' => '▪️','Hex' => '25AA'), array('iOS2' => '','iOS5' => '▫','iOS7' => '▫️','Hex' => '25AB'), array('iOS2' => '','iOS5' => '🔺','iOS7' => '','Hex' => '1F53A'), array('iOS2' => '','iOS5' => '⬜','iOS7' => '⬜️','Hex' => '2B1C'), array('iOS2' => '','iOS5' => '⬛','iOS7' => '⬛️','Hex' => '2B1B'), array('iOS2' => '','iOS5' => '⚫','iOS7' => '⚫️','Hex' => '26AB'), array('iOS2' => '','iOS5' => '⚪','iOS7' => '⚪️','Hex' => '26AA'), array('iOS2' => '','iOS5' => '🔴','iOS7' => '','Hex' => '1F534'), array('iOS2' => '','iOS5' => '🔵','iOS7' => '','Hex' => '1F535'), array('iOS2' => '','iOS5' => '🔻','iOS7' => '','Hex' => '1F53B'), array('iOS2' => '','iOS5' => '🔶','iOS7' => '','Hex' => '1F536'), array('iOS2' => '','iOS5' => '🔷','iOS7' => '','Hex' => '1F537'), array('iOS2' => '','iOS5' => '🔸','iOS7' => '','Hex' => '1F538'), array('iOS2' => '','iOS5' => '🔹','iOS7' => '','Hex' => '1F539'), array('iOS2' => '','iOS5' => '⁉','iOS7' => '⁉️','Hex' => '2049'), array('iOS2' => '','iOS5' => '‼','iOS7' => '‼️','Hex' => '203C')); + if ($file == __DIR__.'/token.php') { + $content[27] = ' $releaseTime = \''.$WAToken.'\';'; + } else { + if ($file == __DIR__.'/Constants.php') { + $content[21] = ' const WHATSAPP_VER = \''.trim($WAver).'\'; // The WhatsApp version.'; + $content[22] = ' const WHATSAPP_USER_AGENT = \'WhatsApp/'.trim($WAver).' S40Version/14.26 Device/Nokia302\'; // User agent used in request/registration code.'; + } + } + + $content = implode("\n", $content); + + file_put_contents($file, $content); +} + +/** + * This function generates a paymentLink where you can extend the account-expiration. + * + * @param string $number Your number with international code, e.g. 49123456789 + * @param int $sku The Time in years (1, 3 or 5) you want to extend the account-expiration. + * + * @return string Returns the link. + **/ +function generatePaymentLink($number, $sku) +{ + return sprintf('https://www.whatsapp.com/payments/cksum_pay.php?phone=%s&cksum=%s&sku=%d', + $number, + md5($number.'abc'), + $sku, + ($sku != 1 && $sku != 3 && $sku != 5) ? 1 : $sku); } -function updateData($nameFile, $WAver, $classesMD5 = "") +// Gets mime type of a file using various methods +function get_mime($file) { - $file = $nameFile; - $open = fopen($file, 'r+'); - $content = fread($open,filesize($file)); - fclose($open); + if (function_exists('finfo_file')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $file); + finfo_close($finfo); - $content = explode("\n",$content); + return $mime; + } - if ($file == 'token.php') - $content[5] = ' $classesMd5 = '."\"".$classesMD5."\"; // $WAver"; - else if ($file == 'whatsprot.class.php'){ - $content[48] = ' const WHATSAPP_VER = \''.$WAver. '\'; // The WhatsApp version.'; - $content[49] = ' const WHATSAPP_USER_AGENT = \'WhatsApp/'.$WAver.' Android/4.3 Device/GalaxyS3\'; // User agent used in request/registration code.'; - } + if (function_exists('mime_content_type')) { + return mime_content_type($file); + } - $content = implode("\n",$content); + if (!strncasecmp(PHP_OS, 'WIN', 3) == 0 && !stristr(ini_get('disable_functions'), 'shell_exec')) { + $file = escapeshellarg($file); + $mime = shell_exec('file -b --mime-type '.$file); - $open = fopen($file,'w'); - fwrite($open,$content); - fclose($open); + return trim($mime); + } + + return false; +} + +function getExtensionFromMime($mime) +{ + $extensions = [ + 'audio/3gpp' => '3gp', + 'audio/x-caf' => 'caf', + 'audio/wav' => 'wav', + 'audio/mpeg' => 'mp3', + 'audio/mpeg3' => 'mp3', + 'audio/x-mpeg-3' => 'mp3', + 'audio/x-ms-wma' => 'wma', + 'audio/ogg' => 'ogg', + 'audio/aiff' => 'aif', + 'audio/x-aiff' => 'aif', + 'audio/aac' => 'aac', + 'audio/mp4' => 'm4a', + 'image/jpeg' => 'jpg', + 'image/gif' => 'gif', + 'image/png' => 'png', + 'video/3gpp' => '3gp', + 'video/mp4' => 'mp4', + 'video/quicktime' => 'mov', + 'video/avi' => 'avi', + 'video/msvideo' => 'avi', + 'video/x-msvideo' => 'avi', + ]; + + return $extensions[$mime]; +} + +function adjustId($id) +{ + $data = strrev(pack('L', $id)); + $data = bin2hex(ltrim($data)); + while (strlen($data) < 6) { + $data = '0'.$data; + } + + return hex2bin($data); +} + +function deAdjustId($data) +{ + if (strlen($data) < 4) { + $data = "\x00".$data; + } + $data = strrev($data); + $data = unpack('L', $data); + + return $data[1]; +} + +function unpadV2Plaintext($v2plaintext) +{ + if (strlen($v2plaintext) < 128) { + return substr($v2plaintext, 2, -1); + } else { + return substr($v2plaintext, 3, -1); + } +} +function parseText($txt) +{ + for ($x = 0; $x < strlen($txt); $x++) { + if (ord($txt[$x]) < 20 || ord($txt[$x]) > 230) { + $txt = 'HEX:'.bin2hex($txt); + + return $txt; + } + } + + return $txt; +} +function niceVarDump($obj, $ident = 0) +{ + $data = ''; + $data .= str_repeat(' ', $ident); + $original_ident = $ident; + $toClose = false; + switch (gettype($obj)) { + case 'object': + $vars = (array) $obj; + $data .= gettype($obj).' ('.get_class($obj).') ('.count($vars).") {\n"; + $ident += 2; + foreach ($vars as $key => $var) { + $type = ''; + $k = bin2hex($key); + if (strpos($k, '002a00') === 0) { + $k = str_replace('002a00', '', $k); + $type = ':protected'; + } elseif (strpos($k, bin2hex("\x00".get_class($obj)."\x00")) === 0) { + $k = str_replace(bin2hex("\x00".get_class($obj)."\x00"), '', $k); + $type = ':private'; + } + $k = hex2bin($k); + if (is_subclass_of($obj, 'ProtobufMessage') && $k == 'values') { + $r = new ReflectionClass($obj); + $constants = $r->getConstants(); + $newVar = []; + foreach ($constants as $ckey => $cval) { + if (substr($ckey, 0, 3) != 'PB_') { + $newVar[$ckey] = $var[$cval]; + } + } + $var = $newVar; + } + $data .= str_repeat(' ', $ident)."[$k$type]=>\n".niceVarDump($var, $ident)."\n"; + } + $toClose = true; + break; + case 'array': + $data .= 'array ('.count($obj).") {\n"; + $ident += 2; + foreach ($obj as $key => $val) { + $data .= str_repeat(' ', $ident).'['.(is_int($key) ? $key : "\"$key\"")."]=>\n".niceVarDump($val, $ident)."\n"; + } + $toClose = true; + break; + case 'string': + $data .= 'string "'.parseText($obj)."\"\n"; + break; + case 'NULL': + $data .= gettype($obj); + break; + default: + $data .= gettype($obj).'('.strval($obj).")\n"; + break; + } + if ($toClose) { + $data .= str_repeat(' ', $original_ident)."}\n"; + } + + return $data; +} +function encodeInt7bit($value) +{ + $v = $value; + $out = ''; + while ($v >= 0x80) { + $out .= chr(($v | 0x80) % 256); + $v >>= 7; + } + $out .= chr($v % 256); + + return $out; +} + +function padMessage($plaintext) +{ + $padded = ''; + $padded .= chr(10); + $padded .= encodeInt7bit(strlen($plaintext)); + $padded .= $plaintext; + $padded .= chr(1); + + return $padded; +} + +function randomStr($length) +{ + $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + $string = ''; + for ($i = 0; $i < $length; $i++) { + $string .= $characters[mt_rand(0, strlen($characters) - 1)]; + } + + return $string; +} + +function getRandomGCM() +{ + return randomStr(5).'_'.randomStr(5).':'.'APA91b'.randomStr(64). + '_'.randomStr(5).'-'.randomStr(12).'_'.randomStr(9). + '_'.randomStr(11).'_'.randomStr(1).'_'.randomStr(26); } diff --git a/src/handlers/Handler.php b/src/handlers/Handler.php new file mode 100644 index 00000000..9e127981 --- /dev/null +++ b/src/handlers/Handler.php @@ -0,0 +1,6 @@ +node = $node; + $this->parent = $parent; + $this->phoneNumber = $this->parent->getMyNumber(); + } + + public function Process() + { + if ($this->node->getChild('query') != null) { + if (isset($this->parent->getNodeId()['privacy']) && ($this->parent->getNodeId()['privacy'] == $this->node->getAttribute('id'))) { + $listChild = $this->node->getChild(0)->getChild(0); + $blockedJids = []; + foreach ($listChild->getChildren() as $child) { + $blockedJids[] = $child->getAttribute('value'); + } + $this->parent->eventManager()->fire('onGetPrivacyBlockedList', + [ + $this->phoneNumber, + $blockedJids, + ]); + + return; + } + } + + if ($this->node->getAttribute('type') == 'get' + && $this->node->getAttribute('xmlns') == 'urn:xmpp:ping') { + $this->parent->eventManager()->fire('onPing', + [ + $this->phoneNumber, + $this->node->getAttribute('id'), + ]); + $this->parent->sendPong($this->node->getAttribute('id')); + } + + if ($this->node->getChild('sync') != null) { + + //sync result + $sync = $this->node->getChild('sync'); + $existing = $sync->getChild('in'); + $nonexisting = $sync->getChild('out'); + + //process existing first + $existingUsers = []; + if (!empty($existing)) { + foreach ($existing->getChildren() as $child) { + $existingUsers[$child->getData()] = $child->getAttribute('jid'); + } + } + + //now process failed numbers + $failedNumbers = []; + if (!empty($nonexisting)) { + foreach ($nonexisting->getChildren() as $child) { + $failedNumbers[] = str_replace('+', '', $child->getData()); + } + } + + $index = $sync->getAttribute('index'); + + $result = new SyncResult($index, $sync->getAttribute('sid'), $existingUsers, $failedNumbers); + + $this->parent->eventManager()->fire('onGetSyncResult', + [ + $result, + ]); + } + + if ($this->node->getChild('props') != null) { + //server properties + $props = []; + foreach ($this->node->getChild(0)->getChildren() as $child) { + $props[$child->getAttribute('name')] = $child->getAttribute('value'); + } + $this->parent->eventManager()->fire('onGetServerProperties', + [ + $this->phoneNumber, + $this->node->getChild(0)->getAttribute('version'), + $props, + ]); + } + if ($this->node->getChild('picture') != null) { + $this->parent->eventManager()->fire('onGetProfilePicture', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getChild('picture')->getAttribute('type'), + $this->node->getChild('picture')->getData(), + ]); + } + if ($this->node->getChild('media') != null || $this->node->getChild('duplicate') != null) { + $this->parent->processUploadResponse($this->node); + } + if (strpos($this->node->getAttribute('from'), Constants::WHATSAPP_GROUP_SERVER) !== false) { + //There are multiple types of Group reponses. Also a valid group response can have NO children. + //Events fired depend on text in the ID field. + $groupList = []; + $groupNodes = []; + if ($this->node->getChild(0) != null && $this->node->getChild(0)->getChildren() != null) { + foreach ($this->node->getChild(0)->getChildren() as $child) { + $groupList[] = $child->getAttributes(); + $groupNodes[] = $child; + } + } + if (isset($this->parent->getNodeId()['groupcreate']) && ($this->parent->getNodeId()['groupcreate'] == $this->node->getAttribute('id'))) { + $this->parent->setGroupId($this->node->getChild(0)->getAttribute('id')); + $this->parent->eventManager()->fire('onGroupsChatCreate', + [ + $this->phoneNumber, + $this->node->getChild(0)->getAttribute('id'), + ]); + } + if (isset($this->parent->getNodeId()['leavegroup']) && ($this->parent->getNodeId()['leavegroup'] == $this->node->getAttribute('id'))) { + $this->parent->setGroupId($this->node->getChild(0)->getChild(0)->getAttribute('id')); + $this->parent->eventManager()->fire('onGroupsChatEnd', + [ + $this->phoneNumber, + $this->node->getChild(0)->getChild(0)->getAttribute('id'), + ]); + } + if (isset($this->parent->getNodeId()['getgroups']) && ($this->parent->getNodeId()['getgroups'] == $this->node->getAttribute('id'))) { + $this->parent->eventManager()->fire('onGetGroups', + [ + $this->phoneNumber, + $groupList, + ]); + //getGroups returns a array of nodes which are exactly the same as from getGroupV2Info + //so lets call this event, we have all data at hand, no need to call getGroupV2Info for every + //group we are interested + foreach ($groupNodes as $groupNode) { + $this->handleGroupV2InfoResponse($groupNode, true); + } + } + if (isset($this->parent->getNodeId()['get_groupv2_info']) && ($this->parent->getNodeId()['get_groupv2_info'] == $this->node->getAttribute('id'))) { + $groupChild = $this->node->getChild(0); + if ($groupChild != null) { + $this->handleGroupV2InfoResponse($groupChild); + } + } + } + if (isset($this->parent->getNodeId()['get_lists']) && ($this->parent->getNodeId()['get_lists'] == $this->node->getAttribute('id'))) { + $broadcastLists = []; + if ($this->node->getChild(0) != null) { + $childArray = $this->node->getChildren(); + foreach ($childArray as $list) { + if ($list->getChildren() != null) { + foreach ($list->getChildren() as $sublist) { + $id = $sublist->getAttribute('id'); + $name = $sublist->getAttribute('name'); + $broadcastLists[$id]['name'] = $name; + $recipients = []; + foreach ($sublist->getChildren() as $recipient) { + array_push($recipients, $recipient->getAttribute('jid')); + } + $broadcastLists[$id]['recipients'] = $recipients; + } + } + } + } + $this->parent->eventManager()->fire('onGetBroadcastLists', + [ + $this->phoneNumber, + $broadcastLists, + ]); + } + if ($this->node->getChild('pricing') != null) { + $this->parent->eventManager()->fire('onGetServicePricing', + [ + $this->phoneNumber, + $this->node->getChild(0)->getAttribute('price'), + $this->node->getChild(0)->getAttribute('cost'), + $this->node->getChild(0)->getAttribute('currency'), + $this->node->getChild(0)->getAttribute('expiration'), + ]); + } + if ($this->node->getChild('extend') != null) { + $this->parent->eventManager()->fire('onGetExtendAccount', + [ + $this->phoneNumber, + $this->node->getChild('account')->getAttribute('kind'), + $this->node->getChild('account')->getAttribute('status'), + $this->node->getChild('account')->getAttribute('creation'), + $this->node->getChild('account')->getAttribute('expiration'), + ]); + } + if ($this->node->getChild('normalize') != null) { + $this->parent->eventManager()->fire('onGetNormalizedJid', + [ + $this->phoneNumber, + $this->node->getChild(0)->getAttribute('result'), + ]); + } + if ($this->node->getChild('status') != null) { + $child = $this->node->getChild('status'); + $childs = $child->getChildren(); + if (isset($childs) && !is_null($childs)) { + foreach ($childs as $status) { + $this->parent->eventManager()->fire('onGetStatus', + [ + $this->phoneNumber, + $status->getAttribute('jid'), + 'requested', + $this->node->getAttribute('id'), + $status->getAttribute('t'), + $status->getData(), + ]); + } + } + } + + if (($this->node->getAttribute('type') == 'error') && ($this->node->getChild('error') != null)) { + $errorType = null; + $this->parent->logFile('error', 'Iq error with {id} id', ['id' => $this->node->getAttribute('id')]); + foreach ($this->parent->getNodeId() as $type => $nodeID) { + if ($nodeID == $this->node->getAttribute('id')) { + $errorType = $type; + break; + } + } + $nodeIds = $this->parent->getNodeId(); + if (isset($nodeIds['sendcipherKeys']) && (isset($nodeIds['sendcipherKeys']) == $this->node->getAttribute('id')) && $this->node->getChild('error')->getAttribute('code') == '406') { + $this->parent->sendSetPreKeys(); + } elseif ($this->node->getAttribute('id') == '2') { + //$this->parent->sendSetGCM(); + } + + $this->parent->eventManager()->fire('onGetError', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getChild(0), + $errorType, + ]); + } + + if (isset($this->parent->getNodeId()['cipherKeys']) && ($this->parent->getNodeId()['cipherKeys'] == $this->node->getAttribute('id'))) { + $users = $this->node->getChild(0)->getChildren(); + foreach ($users as $user) { + $jid = $user->getAttribute('jid'); + $registrationId = deAdjustId($user->getChild('registration')->getData()); + $identityKey = new IdentityKey(new DjbECPublicKey($user->getChild('identity')->getData())); + $signedPreKeyId = deAdjustId($user->getChild('skey')->getChild('id')->getData()); + $signedPreKeyPub = new DjbECPublicKey($user->getChild('skey')->getChild('value')->getData()); + $signedPreKeySig = $user->getChild('skey')->getChild('signature')->getData(); + + $preKeyId = null; + $preKeyPublic = null; + if(!is_null($user->getChild('key'))){ + //SupportsV3 only + $preKeyId = deAdjustId($user->getChild('key')->getChild('id')->getData()); + $preKeyPublic = new DjbECPublicKey($user->getChild('key')->getChild('value')->getData()); + } + + $preKeyBundle = new PreKeyBundle($registrationId, 1, $preKeyId, $preKeyPublic, $signedPreKeyId, $signedPreKeyPub, $signedPreKeySig, $identityKey); + $sessionBuilder = new SessionBuilder($this->parent->getAxolotlStore(), $this->parent->getAxolotlStore(), $this->parent->getAxolotlStore(), $this->parent->getAxolotlStore(), ExtractNumber($jid), 1); + + $sessionBuilder->processPreKeyBundle($preKeyBundle); + if (isset($this->parent->getPendingNodes()[ExtractNumber($jid)])) { + foreach ($this->parent->getPendingNodes()[ExtractNumber($jid)] as $pendingNode) { + $msgHandler = new MessageHandler($this->parent, $pendingNode); + $msgHandler->Process(); + } + $this->parent->unsetPendingNode($jid); + } + $this->parent->sendPendingMessages($jid); + } + } + } + + /** + * @param ProtocolNode $groupNode + * @param mixed $fromGetGroups + */ + protected function handleGroupV2InfoResponse(ProtocolNode $groupNode, $fromGetGroups = false) + { + $creator = $groupNode->getAttribute('creator'); + $creation = $groupNode->getAttribute('creation'); + $subject = $groupNode->getAttribute('subject'); + $groupID = $groupNode->getAttribute('id'); + $participants = []; + $admins = []; + if ($groupNode->getChild(0) != null) { + foreach ($groupNode->getChildren() as $child) { + $participants[] = $child->getAttribute('jid'); + if ($child->getAttribute('type') == 'admin') { + $admins[] = $child->getAttribute('jid'); + } + } + } + $this->parent->eventManager()->fire('onGetGroupV2Info', + [ + $this->phoneNumber, + $groupID, + $creator, + $creation, + $subject, + $participants, + $admins, + $fromGetGroups, + ] + ); + } +} + +class SyncResult +{ + public $index; + public $syncId; + /** @var array $existing */ + public $existing; + /** @var array $nonExisting */ + public $nonExisting; + + public function __construct($index, $syncId, $existing, $nonExisting) + { + $this->index = $index; + $this->syncId = $syncId; + $this->existing = $existing; + $this->nonExisting = $nonExisting; + } +} diff --git a/src/handlers/MessageHandler.php b/src/handlers/MessageHandler.php new file mode 100644 index 00000000..cafe296e --- /dev/null +++ b/src/handlers/MessageHandler.php @@ -0,0 +1,711 @@ +node = $node; + $this->parent = $parent; + $this->phoneNumber = $this->parent->getMyNumber(); + } + + public function Process() + { + $this->parent->pushMessageToQueue($this->node); + + if ($this->node->hasChild('x') && $this->parent->getLastId() == $this->node->getAttribute('id')) { + $this->parent->sendNextMessage(); + } + if ($this->parent->getNewMsgBind() && ($this->node->getChild('body') || $this->node->getChild('media'))) { + $this->parent->getNewMsgBind()->process($this->node); + } + if ($this->node->getAttribute('type') == 'text' && ($this->node->getChild('body') != null || $this->node->getChild('enc') != null)) { + $this->processMessageNode($this->node); + } + if ($this->node->getAttribute('type') == 'media' && ($this->node->getChild('media') != null || $this->node->getChild('enc') != null)) { + $file_data = ''; + if ($this->node->getChild('enc') != null && $this->node->getAttribute('participant') == null) { // for now only private messages + + $dec_node = null; + if (extension_loaded('curve25519') && extension_loaded('protobuf')) { + $dec_node = $this->processEncryptedNode($this->node); + } + if ($dec_node) { + $this->node = $dec_node; + if ($dec_node->getChild('media') != null) { + $file_data = $dec_node->getChild('media')->getAttribute('file'); + } + } + } elseif (($this->node->getChild('enc') == null) && ($this->node->getChild('media')->getAttribute('url') != null)) { + $file_data = file_get_contents($this->node->getChild('media')->getAttribute('url')); + } + + if ($this->node->getChild('enc') != null && $this->node->getChild('enc')->getAttribute('mediatype') == 'url') { + $this->parent->eventManager()->fire('onGetMessage', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $this->node->getChild('body')->getData(), + ]); + } + + if ($this->node->getChild('media') != null) { + if ($this->node->getChild('media')->getAttribute('type') == 'image') { + if ($this->node->getAttribute('participant') == null) { + $this->parent->eventManager()->fire('onGetImage', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $this->node->getChild('media')->getAttribute('size'), + $this->node->getChild('media')->getAttribute('url'), + $file_data, + $this->node->getChild('media')->getAttribute('mimetype'), + $this->node->getChild('media')->getAttribute('filehash'), + $this->node->getChild('media')->getAttribute('width'), + $this->node->getChild('media')->getAttribute('height'), + $this->node->getChild('media')->getData(), + $this->node->getChild('media')->getAttribute('caption'), + ]); + } else { + $this->parent->eventManager()->fire('onGetGroupImage', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('participant'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $this->node->getChild('media')->getAttribute('size'), + $this->node->getChild('media')->getAttribute('url'), + $this->node->getChild('media')->getAttribute('file'), + $this->node->getChild('media')->getAttribute('mimetype'), + $this->node->getChild('media')->getAttribute('filehash'), + $this->node->getChild('media')->getAttribute('width'), + $this->node->getChild('media')->getAttribute('height'), + $this->node->getChild('media')->getData(), + $this->node->getChild('media')->getAttribute('caption'), + ]); + } + + $msgId = $this->parent->createIqId(); + $ackNode = new ProtocolNode('ack', + [ + 'url' => $this->node->getChild('media')->getAttribute('url'), + ], null, null); + + $iqNode = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'w:m', + 'type' => 'set', + 'to' => Constants::WHATSAPP_SERVER, + ], [$ackNode], null); + + $this->parent->sendNode($iqNode); + } elseif ($this->node->getChild('media')->getAttribute('type') == 'video') { + if ($this->node->getAttribute('participant') == null) { + $this->parent->eventManager()->fire('onGetVideo', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $this->node->getChild('media')->getAttribute('url'), + $this->node->getChild('media')->getAttribute('file'), + $this->node->getChild('media')->getAttribute('size'), + $this->node->getChild('media')->getAttribute('mimetype'), + $this->node->getChild('media')->getAttribute('filehash'), + $this->node->getChild('media')->getAttribute('duration'), + $this->node->getChild('media')->getAttribute('vcodec'), + $this->node->getChild('media')->getAttribute('acodec'), + $this->node->getChild('media')->getData(), + $this->node->getChild('media')->getAttribute('caption'), + $this->node->getChild('media')->getAttribute('width'), + $this->node->getChild('media')->getAttribute('height'), + $this->node->getChild('media')->getAttribute('fps'), + $this->node->getChild('media')->getAttribute('vbitrate'), + $this->node->getChild('media')->getAttribute('asampfreq'), + $this->node->getChild('media')->getAttribute('asampfmt'), + $this->node->getChild('media')->getAttribute('abitrate'), + ]); + } else { + $this->parent->eventManager()->fire('onGetGroupVideo', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('participant'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $this->node->getChild('media')->getAttribute('url'), + $this->node->getChild('media')->getAttribute('file'), + $this->node->getChild('media')->getAttribute('size'), + $this->node->getChild('media')->getAttribute('mimetype'), + $this->node->getChild('media')->getAttribute('filehash'), + $this->node->getChild('media')->getAttribute('duration'), + $this->node->getChild('media')->getAttribute('vcodec'), + $this->node->getChild('media')->getAttribute('acodec'), + $this->node->getChild('media')->getData(), + $this->node->getChild('media')->getAttribute('caption'), + $this->node->getChild('media')->getAttribute('width'), + $this->node->getChild('media')->getAttribute('height'), + $this->node->getChild('media')->getAttribute('fps'), + $this->node->getChild('media')->getAttribute('vbitrate'), + $this->node->getChild('media')->getAttribute('asampfreq'), + $this->node->getChild('media')->getAttribute('asampfmt'), + $this->node->getChild('media')->getAttribute('abitrate'), + ]); + } + } elseif ($this->node->getChild('media')->getAttribute('type') == 'audio') { + if ($this->node->getAttribute('participant') == null) { + $this->parent->eventManager()->fire('onGetAudio', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $this->node->getChild('media')->getAttribute('size'), + $this->node->getChild('media')->getAttribute('url'), + $this->node->getChild('media')->getAttribute('file'), + $this->node->getChild('media')->getAttribute('mimetype'), + $this->node->getChild('media')->getAttribute('filehash'), + $this->node->getChild('media')->getAttribute('seconds'), + $this->node->getChild('media')->getAttribute('acodec'), + ]); + } else { + $this->parent->eventManager()->fire('onGetGroupAudio', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('participant'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $this->node->getChild('media')->getAttribute('size'), + $this->node->getChild('media')->getAttribute('url'), + $this->node->getChild('media')->getAttribute('file'), + $this->node->getChild('media')->getAttribute('mimetype'), + $this->node->getChild('media')->getAttribute('filehash'), + $this->node->getChild('media')->getAttribute('seconds'), + $this->node->getChild('media')->getAttribute('acodec'), + ]); + } + } elseif ($this->node->getChild('media')->getAttribute('type') == 'vcard') { + if ($this->node->getChild('media')->hasChild('vcard')) { + $name = $this->node->getChild('media')->getChild('vcard')->getAttribute('name'); + $data = $this->node->getChild('media')->getChild('vcard')->getData(); + } else { + $name = 'NO_NAME'; + $data = $this->node->getChild('media')->getData(); + } + + if ($this->node->getAttribute('participant') == null) { + $this->parent->eventManager()->fire('onGetvCard', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $name, + $data, + ]); + } else { + $this->parent->eventManager()->fire('onGetGroupvCard', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('participant'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $name, + $data, + ]); + } + } elseif ($this->node->getChild('media')->getAttribute('type') == 'location') { + $url = $this->node->getChild('media')->getAttribute('url'); + $name = $this->node->getChild('media')->getAttribute('name'); + if ($this->node->getAttribute('participant') == null) { + $this->parent->eventManager()->fire('onGetLocation', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $name, + $this->node->getChild('media')->getAttribute('longitude'), + $this->node->getChild('media')->getAttribute('latitude'), + $url, + $this->node->getChild('media')->getData(), + ]); + } else { + $this->parent->eventManager()->fire('onGetGroupLocation', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('participant'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('notify'), + $name, + $this->node->getChild('media')->getAttribute('longitude'), + $this->node->getChild('media')->getAttribute('latitude'), + $url, + $this->node->getChild('media')->getData(), + ]); + } + } + } + + //Read receipt for media messages + if ($this->parent->getReadReceipt()) { + $this->parent->sendReceipt($this->node, 'read', $this->node->getAttribute('participant')); + } else { + $this->parent->sendReceipt($this->node, 'receipt', $this->node->getAttribute('participant')); + } + } + if ($this->node->getChild('received') != null) { + $this->parent->eventManager()->fire('onMessageReceivedClient', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('type'), + $this->node->getAttribute('t'), + $this->node->getAttribute('participant'), + ]); + } + } + + protected function processMessageNode(ProtocolNode $node) + { + //encrypted node + if ($node->getChild('enc') != null) { + if (extension_loaded('curve25519') && extension_loaded('protobuf')) { + $ack = new ProtocolNode('ack', ['to' => $node->getAttribute('from'), 'class' => 'message', 'id' => $node->getAttribute('id'), 't' => $node->getAttribute('t')], null, null); + $this->parent->sendNode($ack); + $dec_node = $this->processEncryptedNode($node); + if ($dec_node instanceof ProtocolNode) { + $node = $dec_node; + } + } + } + if ($node) { + $author = $node->getAttribute('participant'); + if ($author == '') { + // Single chats + if ($node->hasChild('body')) { + if ($this->parent->getReadReceipt()) { + $this->parent->sendReceipt($node, 'read', $author); + } else { + $this->parent->sendReceipt($this->node, 'receipt', $author); + } + + $this->parent->eventManager()->fire('onGetMessage', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $node->getAttribute('id'), + $node->getAttribute('type'), + $node->getAttribute('t'), + $node->getAttribute('notify'), + $node->getChild('body')->getData(), + ]); + + if ($this->parent->getMessageStore() !== null) { + $this->parent->getMessageStore()->saveMessage(ExtractNumber($node->getAttribute('from')), $this->phoneNumber, $node->getChild('body')->getData(), $node->getAttribute('id'), $node->getAttribute('t')); + } + } + } else { + //group chat message + if ($node->hasChild('body')) { + if ($this->parent->getReadReceipt()) { + $this->parent->sendReceipt($node, 'read', $author); + } else { + $this->parent->sendReceipt($this->node, 'receipt', $this->node->getAttribute('participant')); + } + + $this->parent->eventManager()->fire('onGetGroupMessage', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $author, + $node->getAttribute('id'), + $node->getAttribute('type'), + $node->getAttribute('t'), + $node->getAttribute('notify'), + $node->getChild('body')->getData(), + ]); + if ($this->parent->getMessageStore() !== null) { + $this->parent->getMessageStore()->saveMessage($author, $node->getAttribute('from'), $node->getChild('body')->getData(), $node->getAttribute('id'), $node->getAttribute('t')); + } + } + } + } + } + + /** + * @param ProtocolNode $node + * + * @return null|ProtocolNode + */ + protected function processEncryptedNode(ProtocolNode $node) + { + if ($this->parent->getAxolotlStore() == null) { + return; + } + //is a chat encrypted message + $from = $node->getAttribute('from'); + if (strpos($from, Constants::WHATSAPP_SERVER) !== false) { + $author = ExtractNumber($node->getAttribute('from')); + + $version = $node->getChild(0)->getAttribute('v'); + $encType = $node->getChild(0)->getAttribute('type'); + $encMsg = $node->getChild('enc')->getData(); + if (!$this->parent->getAxolotlStore()->containsSession($author, 1)) { + //we don't have the session to decrypt, save it in pending and process it later + $this->parent->addPendingNode($node); + $this->parent->logFile('info', 'Requesting cipher keys from {from}', ['from' => $author]); + $this->parent->sendGetCipherKeysFromUser($author); + } else { + //decrypt the message with the session + if ($node->getChild('enc')->getAttribute('count') == '') { + $this->parent->setRetryCounter($node->getAttribute('id'), 1); + } + + if ($version == '2') { + if (!in_array($author, $this->parent->getv2Jids())) { + $this->parent->setv2Jids($author); + } + } + + $plaintext = $this->decryptMessage($from, $encMsg, $encType, $node->getAttribute('id'), $node->getAttribute('t')); + + //$plaintext ="A"; + if ($plaintext === false) { + $this->parent->sendRetry($this->node, $from, $node->getAttribute('id'), $node->getAttribute('t')); + $this->parent->logFile('info', 'Couldn\'t decrypt message with {id} id from {from}. Retrying...', ['id' => $node->getAttribute('id'), 'from' => ExtractNumber($from)]); + + return $node; // could not decrypt + } + if (isset($this->parent->retryNodes[$node->getAttribute('id')])) { + unset($this->parent->retryNodes[$node->getAttribute('id')]); + } + if (isset($this->parent->retryCounters[$node->getAttribute('id')])) { + unset($this->parent->retryCounters[$node->getAttribute('id')]); + } + switch ($node->getAttribute('type')) { + case 'text': + $node->addChild(new ProtocolNode('body', null, null, $plaintext)); + break; + case 'media': + + switch ($node->getChild('enc')->getAttribute('mediatype')) { + case 'image': + $image = new ImageMessage(); + $image->parseFromString($plaintext); + $keys = (new HKDFv3())->deriveSecrets($image->getRefKey(), hex2bin('576861747341707020496d616765204b657973'), 112); + $iv = substr($keys, 0, 16); + $keys = substr($keys, 16); + $parts = str_split($keys, 32); + $key = $parts[0]; + $macKey = $parts[1]; + $refKey = $parts[2]; + + //should be changed to nice curl, no extra headers :D + $file_enc = file_get_contents($image->getUrl()); + //requires mac check , last 10 chars + $mac = substr($file_enc, -10); + $cipherImage = substr($file_enc, 0, strlen($file_enc) - 10); + $decrypted_image = pkcs5_unpad(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $cipherImage, MCRYPT_MODE_CBC, $iv)); + //$save_file = tempnam(sys_get_temp_dir(),"WAIMG_"); + //file_put_contents($save_file,$decrypted_image); + $child = new ProtocolNode('media', + [ + 'size' => $image->getLength(), + 'caption' => $image->getCaption(), + 'url' => $image->getUrl(), + 'mimetype' => $image->getMimeType(), + 'filehash' => bin2hex($image->getSha256()), + 'width' => 0, + 'height' => 0, + 'file' => $decrypted_image ?: $file_enc, + 'type' => 'image', + ], null, $image->getThumbnail()); + $node->addChild($child); + break; + case 'location': + $location = new Location(); + $data = $node->getChild('enc')->getData(); + $location->parseFromString($plaintext); + $child = new ProtocolNode('media', + [ + 'type' => 'location', + 'encoding' => 'raw', + 'latitude' => $location->getLatitude(), + 'longitude' => $location->getLongitude(), + 'name' => $location->getName(), + 'url' => $location->getUrl(), + ], null, $location->getThumbnail()); + $node->addChild($child); + break; + case 'url': + $mediaUrl = new MediaUrl(); + $mediaUrl->parseFromString($plaintext); + $node->addChild(new ProtocolNode('body', null, null, $mediaUrl->getMessage())); + break; + case 'document': + $document = new DocumentMessage(); + $a = ord($plaintext[0]); + //prepad? + if (substr($plaintext, 0, $a) == str_repeat($plaintext[0], $a)) { + $plaintext = substr($plaintext, $a); + } + $document->parseFromString($plaintext); + + $keys = (new HKDFv3())->deriveSecrets($document->getRefKey(), hex2bin('576861747341707020446f63756d656e74204b657973'), 112); + $iv = substr($keys, 0, 16); + $keys = substr($keys, 16); + $parts = str_split($keys, 32); + $key = $parts[0]; + $macKey = $parts[1]; + $refKey = $parts[2]; + //should be changed to nice curl, no extra headers :D + $file_enc = file_get_contents($document->getUrl()); + //requires mac check , last 10 chars + $mac = substr($file_enc, -10); + $cipherDocument = substr($file_enc, 0, strlen($file_enc) - 10); + $uncrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $cipherDocument, MCRYPT_MODE_CBC, $iv); + $decrypted_document = pkcs5_unpad($uncrypted); + file_put_contents('/tmp/'.$document->getFilename(), $decrypted_document); + if (strlen($document->getThumbnail() > 0)) { + //is posible to not have thumbnail + file_put_contents('/tmp/'.$document->getName(), $document->getThumbnail()); + } + break; + } + break; + + } + $this->parent->logFile('info', 'Decrypted message with {id} from {from}', ['id' => $node->getAttribute('id'), 'from' => ExtractNumber($from)]); + + return $node; + } + } + //is a group encrypted message + else { + $author = ExtractNumber($node->getAttribute('participant')); + $group_number = ExtractNumber($node->getAttribute('from')); + $childs = $node->getChildren(); + foreach ($childs as $child) { + if ($child->getAttribute('type') == 'pkmsg' || $child->getAttribute('type') == 'msg') { + if (!$this->parent->getAxolotlStore()->containsSession($author, 1)) { + $this->parent->addPendingNode($node); + + $this->parent->sendGetCipherKeysFromUser($author); + break; + } else { + + //decrypt senderKey and save it + $encType = $child->getAttribute('type'); + $encMsg = $child->getData(); + $from = $node->getAttribute('participant'); + $version = $child->getAttribute('v'); + if ($node->getChild('enc')->getAttribute('count') == '') { + $this->parent->setRetryCounter($node->getAttribute('id'), 1); + } + + if ($version == '2') { + if (!in_array($author, $this->parent->getv2Jids())) { + $this->parent->setv2Jids($author); + } + } + $skip_unpad = $node->getChild('enc', ['type' => 'skmsg']) == null; + $senderKeyBytes = $this->decryptMessage($from, $encMsg, $encType, $node->getAttribute('id'), $node->getAttribute('t'), $node->getAttribute('from'), $skip_unpad); + if ($senderKeyBytes) { + if (!$skip_unpad) { + $senderKeyGroupMessage = new SenderKeyGroupMessage(); + $senderKeyGroupMessage->parseFromString($senderKeyBytes); + } else { + $senderKeyGroupMessage = new SenderKeyGroupData(); + try { + $senderKeyGroupMessage->parseFromString($senderKeyBytes); + } catch (Exception $ex) { + try { + $senderKeyGroupMessage->parseFromString(substr($senderKeyBytes, 0, -1)); + } catch (Exception $ex) { + return $node; + } + } + $message = $senderKeyGroupMessage->getMessage(); + $senderKeyGroupMessage = $senderKeyGroupMessage->getSenderKey(); + } + $senderKey = new SenderKeyDistributionMessage(null, null, null, null, $senderKeyGroupMessage->getSenderKey()); + $groupSessionBuilder = new GroupSessionBuilder($this->parent->getAxolotlStore()); + $groupSessionBuilder->processSender($group_number.':'.$author, $senderKey); + if (isset($message)) { + $this->parent->sendReceipt($node, 'receipt', $this->parent->getJID($this->phoneNumber)); + $node->addChild(new ProtocolNode('body', null, null, $message)); + } + } + } + } elseif ($child->getAttribute('type') == 'skmsg') { + $version = $child->getAttribute('v'); + if ($version == '2') { + if (!in_array($author, $this->parent->v2Jids)) { + $this->parent->setv2Jids($author); + } + } + + $plaintext = $this->decryptMessage([$group_number, $author], $child->getData(), $child->getAttribute('type'), $node->getAttribute('id'), $node->getAttribute('t')); + + if (!$plaintext) { + $this->parent->sendRetry($this->node, $from, $node->getAttribute('id'), $node->getAttribute('t'), $node->getAttribute('participant')); + $this->parent->logFile('info', 'Couldn\'t decrypt group message with {id} id from {from}. Retrying...', ['id' => $node->getAttribute('id'), 'from' => $from]); + + return $node; // could not decrypt + } else { + if (isset($this->parent->retryNodes[$node->getAttribute('id')])) { + unset($this->parent->retryNodes[$node->getAttribute('id')]); + } + if (isset($this->parent->retryCounters[$node->getAttribute('id')])) { + unset($this->parent->retryCounters[$node->getAttribute('id')]); + } + $this->parent->logFile('info', 'Decrypted group message with {id} from {from}', ['id' => $node->getAttribute('id'), 'from' => $from]); + $this->parent->sendReceipt($node, 'receipt', $this->parent->getJID($this->phoneNumber)); + $node->addChild(new ProtocolNode('body', null, null, $plaintext)); + } + } + } + } + + return $node; + } + + public function decryptMessage($from, $ciphertext, $type, $id, $t, $retry_from = null, $skip_unpad = false) + { + $version = '1'; + $this->parent->debugPrint("\n-> Decrypted Message: "); + if ($type == 'pkmsg') { + if (in_array(ExtractNumber($from), $this->parent->getv2Jids())) { + $version = '2'; + } + + try { + $preKeyWhisperMessage = new PreKeyWhisperMessage(null, null, null, null, null, null, null, $ciphertext); + $sessionCipher = $this->parent->getSessionCipher(ExtractNumber($from)); + $plaintext = $sessionCipher->decryptPkmsg($preKeyWhisperMessage); + + if ($version == '2' && !$skip_unpad) { + $plaintext = unpadV2Plaintext($plaintext); + } + $this->parent->debugPrint(parseText($plaintext)."\n\n"); + + return $plaintext; + } catch (Exception $e) { + if ($e instanceof UntrustedIdentityException) { + $this->parent->getAxolotlStore()->clearRecipient(ExtractNumber($from)); + } + $this->parent->debugPrint($e->getMessage().' - '.$e->getFile().' - '.$e->getLine()); + // if ($e->getMessage() != "Null values!"){ + $this->parent->debugPrint("Message $id could not be decrypted, sending retry.\n\n"); + $participant = null; + if ($retry_from != null) { + if (strpos($retry_from, '-') !== false) { + $participant = $from; + } + $from = $retry_from; + } + //$this->sendRetry($from, $id, $t, $participant); + return false; + //} + } + } + // msg, WhisperMessage + elseif ($type == 'msg') { + if (in_array(ExtractNumber($from), $this->parent->getv2Jids())) { + $version = '2'; + } + try { + $whisperMessage = new WhisperMessage(null, null, null, null, null, null, null, null, $ciphertext); + $sessionCipher = $this->parent->getSessionCipher(ExtractNumber($from)); + $plaintext = $sessionCipher->decryptMsg($whisperMessage); + + if ($version == '2' && !$skip_unpad) { + $plaintext = unpadV2Plaintext($plaintext); + } + $this->parent->debugPrint(parseText($plaintext)."\n\n"); + + return $plaintext; + } catch (Exception $e) { + $this->parent->debugPrint($e->getMessage().' - '.$e->getFile().' - '.$e->getLine()); + $this->parent->debugPrint("Message $id could not be decrypted, sending retry.\n\n"); + if ($retry_from != null) { + $from = $retry_from; + } + //$this->sendRetry($from, $id, $t); + return false; + } + } elseif ($type == 'skmsg') { + if (in_array($from[1], $this->parent->v2Jids)) { + $version = '2'; + } + try { + $groupCipher = $this->parent->getGroupCipher(ExtractNumber($from[0]).':'.$from[1]); + $plaintext = $groupCipher->decrypt($ciphertext); + if ($version == '2' && !$skip_unpad) { + $plaintext = unpadV2Plaintext($plaintext); + } + $this->parent->debugPrint("Message $id decrypted to ".parseText($plaintext)."\n\n"); + + return $plaintext; + } catch (Exception $e) { + $this->parent->debugPrint($e->getMessage().' - '.$e->getFile().' - '.$e->getLine()); + if ($retry_from != null) { + $from = $retry_from; + } + $this->parent->sendRetry($this->node, $this->parent->getJID($from[0]), $id, $t); + + return false; + } + } + + return false; + } +} diff --git a/src/handlers/NotificationHandler.php b/src/handlers/NotificationHandler.php new file mode 100644 index 00000000..aa3bd8d4 --- /dev/null +++ b/src/handlers/NotificationHandler.php @@ -0,0 +1,217 @@ +node = $node; + $this->type = $node->getAttribute('type'); + $this->parent = $parent; + $this->phoneNumber = $this->parent->getMyNumber(); + } + + public function Process() + { + switch ($this->type) { + case 'status': + $this->parent->eventManager()->fire('onGetStatus', + [ + $this->phoneNumber, //my number + $this->node->getAttribute('from'), + $this->node->getChild(0)->getTag(), + $this->node->getAttribute('id'), + $this->node->getAttribute('t'), + $this->node->getChild(0)->getData(), + ]); + break; + case 'picture': + if ($this->node->hasChild('set')) { + $this->parent->eventManager()->fire('onProfilePictureChanged', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('t'), + ]); + } elseif ($this->node->hasChild('delete')) { + $this->parent->eventManager()->fire('onProfilePictureDeleted', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $this->node->getAttribute('t'), + ]); + } + //TODO + break; + case 'contacts': + $notification = $this->node->getChild(0)->getTag(); + if ($notification == 'add') { + $this->parent->eventManager()->fire('onNumberWasAdded', + [ + $this->phoneNumber, + $this->node->getChild(0)->getAttribute('jid'), + ]); + } elseif ($notification == 'remove') { + $this->parent->eventManager()->fire('onNumberWasRemoved', + [ + $this->phoneNumber, + $this->node->getChild(0)->getAttribute('jid'), + ]); + } elseif ($notification == 'update') { + $this->parent->eventManager()->fire('onNumberWasUpdated', + [ + $this->phoneNumber, + $this->node->getChild(0)->getAttribute('jid'), + ]); + } + break; + case 'encrypt': + if (extension_loaded('curve25519') && extension_loaded('protobuf')) { + $value = $this->node->getChild(0)->getAttribute('value'); + if (is_numeric($value)) { + $this->parent->getAxolotlStore()->removeAllPrekeys(); + $this->parent->sendSetPreKeys(true); + } else { + echo 'Corrupt Stream: value '.$value.'is not numeric'; + } + } + break; + case 'w:gp2': + if ($this->node->hasChild('remove')) { + if ($this->node->getChild(0)->hasChild('participant')) { + $this->parent->eventManager()->fire('onGroupsParticipantsRemove', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getChild(0)->getChild(0)->getAttribute('jid'), + ]); + } + } elseif ($this->node->hasChild('add')) { + $this->parent->eventManager()->fire('onGroupsParticipantsAdd', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getChild(0)->getChild(0)->getAttribute('jid'), + ]); + } elseif ($this->node->hasChild('create')) { + $groupMembers = []; + foreach ($this->node->getChild(0)->getChild(0)->getChildren() as $cn) { + $groupMembers[] = $cn->getAttribute('jid'); + } + $this->parent->eventManager()->fire('onGroupisCreated', + [ + $this->phoneNumber, + $this->node->getChild(0)->getChild(0)->getAttribute('creator'), + $this->node->getChild(0)->getChild(0)->getAttribute('id'), + $this->node->getChild(0)->getChild(0)->getAttribute('subject'), + $this->node->getAttribute('participant'), + $this->node->getChild(0)->getChild(0)->getAttribute('creation'), + $groupMembers, + ]); + } elseif ($this->node->hasChild('subject')) { + $this->parent->eventManager()->fire('onGetGroupsSubject', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('t'), + $this->node->getAttribute('participant'), + $this->node->getAttribute('notify'), + $this->node->getChild(0)->getAttribute('subject'), + ]); + } elseif ($this->node->hasChild('promote')) { + $promotedJIDs = []; + foreach ($this->node->getChild(0)->getChildren() as $cn) { + $promotedJIDs[] = $cn->getAttribute('jid'); + } + $this->parent->eventManager()->fire('onGroupsParticipantsPromote', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), //Group-JID + $this->node->getAttribute('t'), //Time + $this->node->getAttribute('participant'), //Issuer-JID + $this->node->getAttribute('notify'), //Issuer-Name + $promotedJIDs, + ] + ); + } elseif ($this->node->hasChild('demote')) { + $demotedJIDs = []; + foreach ($this->node->getChild(0)->getChildren() as $cn) { + $demotedJIDs[] = $cn->getAttribute('jid'); + } + $this->parent->eventManager()->fire('onGroupsParticipantsDemote', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), //Group-JID + $this->node->getAttribute('t'), //Time + $this->node->getAttribute('participant'), //Issuer-JID + $this->node->getAttribute('notify'), //Issuer-Name + $demotedJIDs, + ] + ); + } elseif ($this->node->hasChild('modify')) { + $this->parent->eventManager()->fire('onGroupsParticipantChangedNumber', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('t'), + $this->node->getAttribute('participant'), + $this->node->getAttribute('notify'), + $this->node->getChild(0)->getChild(0)->getAttribute('jid'), + ] + ); + } + break; + case 'account': + if (($this->node->getChild(0)->getAttribute('author')) == '') { + $author = 'Paypal'; + } else { + $author = $this->node->getChild(0)->getAttribute('author'); + } + $this->parent->eventManager()->fire('onPaidAccount', + [ + $this->phoneNumber, + $author, + $this->node->getChild(0)->getChild(0)->getAttribute('kind'), + $this->node->getChild(0)->getChild(0)->getAttribute('status'), + $this->node->getChild(0)->getChild(0)->getAttribute('creation'), + $this->node->getChild(0)->getChild(0)->getAttribute('expiration'), + ]); + break; + case 'features': + if ($this->node->getChild(0)->getChild(0) == 'encrypt') { + $this->parent->eventManager()->fire('onGetFeature', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getChild(0)->getChild(0)->getAttribute('value'), + ]); + } + break; + case 'web': + if (($this->node->getChild(0)->getTag() == 'action') && ($this->node->getChild(0)->getAttribute('type') == 'sync')) { + $data = $this->node->getChild(0)->getChildren(); + $this->parent->eventManager()->fire('onWebSync', + [ + $this->phoneNumber, + $this->node->getAttribute('from'), + $this->node->getAttribute('id'), + $data[0]->getData(), + $data[1]->getData(), + $data[2]->getData(), + ]); + } + break; + default: + throw new Exception("Method $this->type not implemented"); + } + $this->parent->sendAck($this->node, 'notification'); + } +} diff --git a/src/keystream.class.php b/src/keystream.class.php index 97914672..ce8efe0b 100755 --- a/src/keystream.class.php +++ b/src/keystream.class.php @@ -6,15 +6,17 @@ * Time: 11:55 * To change this template use File | Settings | File Templates. */ -require_once("rc4.php"); -require_once("func.php"); +require_once 'rc4.php'; +require_once 'func.php'; -class KeyStream { - public static $AuthMethod = "WAUTH-2"; +class KeyStream +{ + public static $AuthMethod = 'WAUTH-2'; const DROP = 768; private $rc4; - private $seq; + private $seq = 0; private $macKey; + //key = password, mackey = challengedata public function __construct($key, $macKey) { @@ -24,20 +26,20 @@ public function __construct($key, $macKey) public static function GenerateKeys($password, $nonce) { - $array = array( - "key",//placeholders - "key", - "key", - "key" - ); - $array2 = array(1, 2, 3, 4); + $array = [ + 'key', //placeholders + 'key', + 'key', + 'key', + ]; + $array2 = [1, 2, 3, 4]; $nonce .= '0'; - for($j = 0; $j < count($array); $j++) - { + for ($j = 0; $j < count($array); $j++) { $nonce[(strlen($nonce) - 1)] = chr($array2[$j]); - $foo = wa_pbkdf2("sha1", $password, $nonce, 2, 20, true); + $foo = wa_pbkdf2('sha1', $password, $nonce, 2, 20, true); $array[$j] = $foo; } + return $array; } @@ -45,15 +47,14 @@ public function DecodeMessage($buffer, $macOffset, $offset, $length) { $mac = $this->computeMac($buffer, $offset, $length); //validate mac - for($i = 0; $i < 4; $i++) - { + for ($i = 0; $i < 4; $i++) { $foo = ord($buffer[$macOffset + $i]); $bar = ord($mac[$i]); - if($foo !== $bar) - { + if ($foo !== $bar) { throw new Exception("MAC mismatch: $foo != $bar"); } } + return $this->rc4->cipher($buffer, $offset, $length); } @@ -61,19 +62,21 @@ public function EncodeMessage($buffer, $macOffset, $offset, $length) { $data = $this->rc4->cipher($buffer, $offset, $length); $mac = $this->computeMac($data, $offset, $length); + return substr($data, 0, $macOffset).substr($mac, 0, 4).substr($data, $macOffset + 4); } private function computeMac($buffer, $offset, $length) { - $hmac = hash_init("sha1", HASH_HMAC, $this->macKey); + $hmac = hash_init('sha1', HASH_HMAC, $this->macKey); hash_update($hmac, substr($buffer, $offset, $length)); $array = chr($this->seq >> 24) - . chr($this->seq >> 16) - . chr($this->seq >> 8) - . chr($this->seq); + .chr($this->seq >> 16) + .chr($this->seq >> 8) + .chr($this->seq); hash_update($hmac, $array); $this->seq++; + return hash_final($hmac, true); } -} \ No newline at end of file +} diff --git a/src/libaxolotl-php/.gitignore b/src/libaxolotl-php/.gitignore new file mode 100755 index 00000000..0daaa6cf --- /dev/null +++ b/src/libaxolotl-php/.gitignore @@ -0,0 +1,9 @@ +/bootstrap/compiled.php +/vendor +/.idea +/nbproject +composer.phar +.env.*.php +.env.php +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/src/libaxolotl-php/DuplicateMessageException.php b/src/libaxolotl-php/DuplicateMessageException.php new file mode 100755 index 00000000..d2b826a0 --- /dev/null +++ b/src/libaxolotl-php/DuplicateMessageException.php @@ -0,0 +1,9 @@ +message = $s; + } +} diff --git a/src/libaxolotl-php/IdentityKey.php b/src/libaxolotl-php/IdentityKey.php new file mode 100755 index 00000000..7faf26f0 --- /dev/null +++ b/src/libaxolotl-php/IdentityKey.php @@ -0,0 +1,52 @@ +publicKey = $publicKeyOrBytes; + } else { + $this->publicKey = Curve::decodePoint($publicKeyOrBytes, $offset); + } + } + + public function getPublicKey() + { + return $this->publicKey; + } + + public function serialize() + { + return $this->publicKey->serialize(); + } + + public function getFingerprint() + { + $hex = unpack('H*', $this->publicKey->serialize()); + $hex = implode(' ', str_split($hex, 2)); + + return $hex; + } + + public function equals($other) // [Object other] + { + if (($other == null)) { + return false; + } + if (!($other instanceof self)) { + return false; + } + + return $this->publicKey->equals($other->getPublicKey()); + } + + public function hashCode() + { + return $this->publicKey->hashCode(); + } +} diff --git a/src/libaxolotl-php/IdentityKeyPair.php b/src/libaxolotl-php/IdentityKeyPair.php new file mode 100755 index 00000000..6dcba40e --- /dev/null +++ b/src/libaxolotl-php/IdentityKeyPair.php @@ -0,0 +1,39 @@ +publicKey = $publicKey; + $this->privateKey = $privateKey; + } else { + $structure = new Textsecure_IdentityKeyPairStructure(); + $structure->parseFromString($serialized); + $this->publicKey = new IdentityKey($structure->getPublicKey(), 0); + $this->privateKey = Curve::decodePrivatePoint($structure->getPrivateKey()); + } + } + + public function getPublicKey() + { + return $this->publicKey; + } + + public function getPrivateKey() + { + return $this->privateKey; + } + + public function serialize() + { + $struct = new Textsecure_IdentityKeyPairStructure(); + + return $struct->setPublicKey((string) $this->publicKey->serialize())->setPrivateKey((string) $this->privateKey->serialize())->serializeToString(); + } +} diff --git a/src/libaxolotl-php/InvalidKeyException.php b/src/libaxolotl-php/InvalidKeyException.php new file mode 100755 index 00000000..e2956ab1 --- /dev/null +++ b/src/libaxolotl-php/InvalidKeyException.php @@ -0,0 +1,9 @@ +message = $detailMessage; + } +} diff --git a/src/libaxolotl-php/InvalidKeyIdException.php b/src/libaxolotl-php/InvalidKeyIdException.php new file mode 100755 index 00000000..6a34b228 --- /dev/null +++ b/src/libaxolotl-php/InvalidKeyIdException.php @@ -0,0 +1,9 @@ +message = $detailMessage; + } +} diff --git a/src/libaxolotl-php/InvalidMacException.php b/src/libaxolotl-php/InvalidMacException.php new file mode 100755 index 00000000..913fe3a5 --- /dev/null +++ b/src/libaxolotl-php/InvalidMacException.php @@ -0,0 +1,9 @@ +message = $detailMessage; + } +} diff --git a/src/libaxolotl-php/InvalidMessageException.php b/src/libaxolotl-php/InvalidMessageException.php new file mode 100755 index 00000000..31af0c59 --- /dev/null +++ b/src/libaxolotl-php/InvalidMessageException.php @@ -0,0 +1,12 @@ +message = $detailMessage; + if ($throw != null) { + $this->previous = $throw; + } + } +} diff --git a/src/libaxolotl-php/InvalidVersionException.php b/src/libaxolotl-php/InvalidVersionException.php new file mode 100755 index 00000000..5a918b41 --- /dev/null +++ b/src/libaxolotl-php/InvalidVersionException.php @@ -0,0 +1,9 @@ +message = $detailMessage; + } +} diff --git a/src/libaxolotl-php/LICENSE b/src/libaxolotl-php/LICENSE new file mode 100755 index 00000000..6b156fe1 --- /dev/null +++ b/src/libaxolotl-php/LICENSE @@ -0,0 +1,675 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {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 . + +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: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/src/libaxolotl-php/LegacyMessageException.php b/src/libaxolotl-php/LegacyMessageException.php new file mode 100755 index 00000000..9656f78d --- /dev/null +++ b/src/libaxolotl-php/LegacyMessageException.php @@ -0,0 +1,9 @@ +message = $detailMesssage; + } +} diff --git a/src/libaxolotl-php/NoSessionException.php b/src/libaxolotl-php/NoSessionException.php new file mode 100755 index 00000000..a601b8bb --- /dev/null +++ b/src/libaxolotl-php/NoSessionException.php @@ -0,0 +1,9 @@ +message = $s; + } +} diff --git a/src/libaxolotl-php/README.md b/src/libaxolotl-php/README.md new file mode 100755 index 00000000..0307c880 --- /dev/null +++ b/src/libaxolotl-php/README.md @@ -0,0 +1,67 @@ +![axolotl-php](http://cl.ly/image/0h3c171L190A/download/axolotlphp.png) + +This is a php port of [libaxolotl-android](https://github.com/WhisperSystems/libaxolotl-android) originally written by [Moxie Marlinspike](https://github.com/moxie0) + +Overview from original author's: + + > This is a ratcheting forward secrecy protocol that works in synchronous and asynchronous messaging environments. The protocol overview is available [here](https://github.com/trevp/axolotl/wiki), and the details of the wire format are available [here](https://github.com/trevp/axolotl/wiki). + +Read rest of of details [here](https://github.com/WhisperSystems/libaxolotl-android/blob/master/README.md). + +# Overview + +This is a ratcheting forward secrecy protocol that works in synchronous and asynchronous messaging +environments. The protocol overview is available [here](https://github.com/trevp/axolotl/wiki), +and the details of the wire format are available [here](https://github.com/WhisperSystems/TextSecure/wiki/ProtocolV2). + +## PreKeys + +This protocol uses a concept called 'PreKeys'. A PreKey is an ECPublicKey and an associated unique +ID which are stored together by a server. PreKeys can also be signed. + +At install time, clients generate a single signed PreKey, as well as a large list of unsigned +PreKeys, and transmit all of them to the server. + +## Sessions + +The axolotl protocol is session-oriented. Clients establish a "session," which is then used for +all subsequent encrypt/decrypt operations. There is no need to ever tear down a session once one +has been established. + +Sessions are established in one of three ways: + +1. PreKeyBundles. A client that wishes to send a message to a recipient can establish a session by + retrieving a PreKeyBundle for that recipient from the server. +1. PreKeyWhisperMessages. A client can receive a PreKeyWhisperMessage from a recipient and use it + to establish a session. +1. KeyExchangeMessages. Two clients can exchange KeyExchange messages to establish a session. + +## State + +An established session encapsulates a lot of state between two clients. That state is maintained +in durable records which need to be kept for the life of the session. + +State is kept in the following places: + +1. Identity State. Clients will need to maintain the state of their own identity key pair, as well + as identity keys received from other clients. +1. PreKey State. Clients will need to maintain the state of their generated PreKeys. +1. Signed PreKey States. Clients will need to maintain the state of their signed PreKeys. +1. Session State. Clients will need to maintain the state of the sessions they have established. + + +# Legal things +## Cryptography Notice + +This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. +BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. +See for more information. + +The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. +The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. + +## License + +Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html + + diff --git a/src/libaxolotl-php/SessionBuilder.php b/src/libaxolotl-php/SessionBuilder.php new file mode 100755 index 00000000..3d665381 --- /dev/null +++ b/src/libaxolotl-php/SessionBuilder.php @@ -0,0 +1,347 @@ +sessionStore = $sessionStore; + $this->preKeyStore = $preKeyStore; + $this->signedPreKeyStore = $signedPreKeyStore; + $this->identityKeyStore = $identityKeyStore; + $this->recipientId = $recepientId; + $this->deviceId = $deviceId; + } + + public function process($sessionRecord, $message) + { + /* + :param sessionRecord: + :param message: + :type message: PreKeyWhisperMessage + */ + + $messageVersion = $message->getMessageVersion(); + $theirIdentityKey = $message->getIdentityKey(); + + $unsignedPreKeyId = null; + + if (!$this->identityKeyStore->isTrustedIdentity($this->recipientId, $theirIdentityKey)) { + throw new UntrustedIdentityException('Untrusted identity!!'); + } + if ($messageVersion == 2) { + $unsignedPreKeyId = $this->processV2($sessionRecord, $message); + } elseif ($messageVersion == 3) { + $unsignedPreKeyId = $this->processV3($sessionRecord, $message); + } else { + throw new Exception('Unkown version '.$messageVersion); + } + + $this->identityKeyStore->saveIdentity($this->recipientId, $theirIdentityKey); + + return $unsignedPreKeyId; + } + + public function processV2($sessionRecord, $message) + { + /* + :type sessionRecord: SessionRecord + :type message: PreKeyWhisperMessage + */ + + if ($message->getPreKeyId() == null) { + throw new InvalidKeyIdException('V2 message requires one time prekey id!'); + } + if (!$this->preKeyStore->containsPreKey($message->getPreKeyId()) && + $this->sessionStore->containsSession($this->recipientId, $this->deviceId)) { + Log::warn('v2', "We've already processed the prekey part of this V2 session, letting bundled message fall through..."); + + return; + } + + $ourPreKey = $this->preKeyStore->loadPreKey($message->getPreKeyId())->getKeyPair(); + + $parameters = (new BobBuilder()); + + $parameters->setOurIdentityKey($this->identityKeyStore->getIdentityKeyPair()) + ->setOurSignedPreKey($ourPreKey) + ->setOurRatchetKey($ourPreKey) + ->setOurOneTimePreKey(null) + ->setTheirIdentityKey($message->getIdentityKey()) + ->setTheirBaseKey($message->getBaseKey()); + + if (!$sessionRecord->isFresh()) { + $sessionRecord->archiveCurrentState(); + } + + RatchetingSession::initializeSessionAsBob($sessionRecord->getSessionState(), $message->getMessageVersion(), $parameters->create()); + + $sessionRecord->getSessionState()->setLocalRegistrationId($this->identityKeyStore->getLocalRegistrationId()); + $sessionRecord->getSessionState()->setRemoteRegistrationId($message->getRegistrationId()); + $sessionRecord->getSessionState()->setAliceBaseKey($message->getBaseKey()->serialize()); + + if ($message->getPreKeyId() != Medium::MAX_VALUE) { + return $message->getPreKeyId(); + } else { + return; + } + } + + public function processV3($sessionRecord, $message) + { + /* + :param sessionRecord: + :param message: + :type message: PreKeyWhisperMessage + :return: + */ + if ($sessionRecord->hasSessionState($message->getMessageVersion(), $message->getBaseKey()->serialize())) { + Log::warn('v3', "We've already setup a session for this V3 message, letting bundled message fall through..."); + + return; + } + + $ourSignedPreKey = $this->signedPreKeyStore->loadSignedPreKey($message->getSignedPreKeyId())->getKeyPair(); + $parameters = new BobBuilder(); + $parameters->setTheirBaseKey($message->getBaseKey()) + ->setTheirIdentityKey($message->getIdentityKey()) + ->setOurIdentityKey($this->identityKeyStore->getIdentityKeyPair()) + ->setOurSignedPreKey($ourSignedPreKey) + ->setOurRatchetKey($ourSignedPreKey); + + if ($message->getPreKeyId() != null) { + $parameters->setOurOneTimePreKey($this->preKeyStore->loadPreKey($message->getPreKeyId())->getKeyPair()); + } else { + $parameters->setOurOneTimePreKey(null); + } + + if (!$sessionRecord->isFresh()) { + $sessionRecord->archiveCurrentState(); + } + + RatchetingSession::initializeSessionAsBob($sessionRecord->getSessionState(), $message->getMessageVersion(), $parameters->create()); + $sessionRecord->getSessionState()->setLocalRegistrationId($this->identityKeyStore->getLocalRegistrationId()); + $sessionRecord->getSessionState()->setRemoteRegistrationId($message->getRegistrationId()); + $sessionRecord->getSessionState()->setAliceBaseKey($message->getBaseKey()->serialize()); + + if ($message->getPreKeyId() != null && $message->getPreKeyId() != Medium::MAX_VALUE) { + return $message->getPreKeyId(); + } else { + return; + } + } + + public function processPreKeyBundle($preKey) + { + /* + :type preKey: PreKeyBundle + */ + if (!$this->identityKeyStore->isTrustedIdentity($this->recipientId, $preKey->getIdentityKey())) { + throw new UntrustedIdentityException(); + } + + if ($preKey->getSignedPreKey() != null && + !Curve::verifySignature($preKey->getIdentityKey()->getPublicKey(), + $preKey->getSignedPreKey()->serialize(), + $preKey->getSignedPreKeySignature())) { + throw new InvalidKeyException('Invalid signature on device key!'); + } + + if ($preKey->getSignedPreKey() == null && $preKey->getPreKey() == null) { + throw new InvalidKeyException('Both signed and unsigned prekeys are absent!'); + } + + $supportsV3 = $preKey->getSignedPreKey() != null; + $sessionRecord = $this->sessionStore->loadSession($this->recipientId, $this->deviceId); + $ourBaseKey = Curve::generateKeyPair(); + $theirSignedPreKey = $supportsV3 ? $preKey->getSignedPreKey() : $preKey->getPreKey(); + $theirOneTimePreKey = $preKey->getPreKey(); + $theirOneTimePreKeyId = $theirOneTimePreKey != null ? $preKey->getPreKeyId() : null; + + $parameters = new AliceBuilder(); + + $parameters->setOurBaseKey($ourBaseKey) + ->setOurIdentityKey($this->identityKeyStore->getIdentityKeyPair()) + ->setTheirIdentityKey($preKey->getIdentityKey()) + ->setTheirSignedPreKey($theirSignedPreKey) + ->setTheirRatchetKey($theirSignedPreKey) + ->setTheirOneTimePreKey($supportsV3 ? $theirOneTimePreKey : null); + + if (!$sessionRecord->isFresh()) { + $sessionRecord->archiveCurrentState(); + } + RatchetingSession::initializeSessionAsAlice($sessionRecord->getSessionState(), + ($supportsV3 ? 3 : 2), + $parameters->create()); + + $sessionRecord->getSessionState()->setUnacknowledgedPreKeyMessage($theirOneTimePreKeyId, $preKey->getSignedPreKeyId(), $ourBaseKey->getPublicKey()); + $sessionRecord->getSessionState()->setLocalRegistrationId($this->identityKeyStore->getLocalRegistrationId()); + $sessionRecord->getSessionState()->setRemoteRegistrationId($preKey->getRegistrationId()); + $sessionRecord->getSessionState()->setAliceBaseKey($ourBaseKey->getPublicKey()->serialize()); + $this->sessionStore->storeSession($this->recipientId, $this->deviceId, $sessionRecord); + $this->identityKeyStore->saveIdentity($this->recipientId, $preKey->getIdentityKey()); + } + + public function processKeyExchangeMessage($keyExchangeMessage) + { + if (!$this->identityKeyStore->isTrustedIdentity($this->recipientId, $keyExchangeMessage->getIdentityKey())) { + throw new UntrustedIdentityException(); + } + + $responseMessage = null; + + if ($keyExchangeMessage->isInitiate()) { + $responseMessage = $this->processInitiate($keyExchangeMessage); + } else { + $this->processResponse($keyExchangeMessage); + } + + return $responseMessage; + } + + public function processInitiate($keyExchangeMessage) + { + $flags = KeyExchangeMessage::RESPONSE_FLAG; + $sessionRecord = $this->sessionStore->loadSession($this->recipientId, $this->deviceId); + + if ($keyExchangeMessage->getVersion() >= 3 && !Curve::verifySignature( + $keyExchangeMessage->getIdentityKey()->getPublicKey(), + $keyExchangeMessage->getBaseKey()->serialize(), + $keyExchangeMessage->getBaseKeySignature())) { + throw new InvalidKeyException('Bad signature!'); + } + + $builder = new SymmetricBuilder(); + if (!$sessionRecord->getSessionState()->hasPendingKeyExchange()) { + $builder->setOurIdentityKey($this->identityKeyStore->getIdentityKeyPair()) + ->setOurBaseKey(Curve::generateKeyPair()) + ->setOurRatchetKey(Curve::generateKeyPair()); + } else { + $builder->setOurIdentityKey($sessionRecord->getSessionState()->getPendingKeyExchangeIdentityKey()) + ->setOurBaseKey($sessionRecord->getSessionState()->getPendingKeyExchangeBaseKey()) + ->setOurRatchetKey($sessionRecord->getSessionState()->getPendingKeyExchangeRatchetKey()); + $flags |= KeyExchangeMessage::SIMULTAENOUS_INITIATE_FLAG; + } + + $builder->setTheirBaseKey($keyExchangeMessage->getBaseKey()) + ->setTheirRatchetKey($keyExchangeMessage->getRatchetKey()) + ->setTheirIdentityKey($keyExchangeMessage->getIdentityKey()); + + $parameters = $builder->create(); + + if (!$sessionRecord->isFresh()) { + $sessionRecord->archiveCurrentState(); + } + + RatchetingSession::initializeSession($sessionRecord->getSessionState(), + min($keyExchangeMessage->getMaxVersion(), CiphertextMessage::CURRENT_VERSION), + $parameters); + + $this->sessionStore->storeSession($this->recipientId, $this->deviceId, $sessionRecord); + $this->identityKeyStore->saveIdentity($this->recipientId, $keyExchangeMessage->getIdentityKey()); + + $baseKeySignature = Curve::calculateSignature($parameters->getOurIdentityKey()->getPrivateKey(), + $parameters->getOurBaseKey()->getPublicKey()->serialize()); + + return new KeyExchangeMessage($sessionRecord->getSessionState()->getSessionVersion(), + $keyExchangeMessage->getSequence(), $flags, + $parameters->getOurBaseKey()->getPublicKey(), + $baseKeySignature, $parameters->getOurRatchetKey()->getPublicKey(), + $parameters->getOurIdentityKey()->getPublicKey()); + } + + public function processResponse($keyExchangeMessage) + { + $sessionRecord = $this->sessionStore->loadSession($this->recipientId, $this->deviceId); + + $sessionState = $sessionRecord->getSessionState(); + $hasPendingKeyExchange = $sessionState->hasPendingKeyExchange(); + $isSimultaneousInitiateResponse = $keyExchangeMessage->isResponseForSimultaneousInitiate(); + + if (!$hasPendingKeyExchange || $sessionState->getPendingKeyExchangeSequence() != $keyExchangeMessage->getSequence()) { + Log::warn('procResponse', 'No matching sequence for response. Is simultaneous initiate response:'.($isSimultaneousInitiateResponse ? 'true' : 'false')); + if (!$isSimultaneousInitiateResponse) { + throw new StaleKeyExchangeException(); + } else { + return; + } + } + + $parameters = new SymmetricBuilder(); + + $parameters->setOurBaseKey($sessionRecord->getSessionState()->getPendingKeyExchangeBaseKey()) + ->setOurRatchetKey($sessionRecord->getSessionState()->getPendingKeyExchangeRatchetKey()) + ->setOurIdentityKey($sessionRecord->getSessionState()->getPendingKeyExchangeIdentityKey()) + ->setTheirBaseKey($keyExchangeMessage->getBaseKey()) + ->setTheirRatchetKey($keyExchangeMessage->getRatchetKey()) + ->setTheirIdentityKey($keyExchangeMessage->getIdentityKey()); + + if (!$sessionRecord->isFresh()) { + $sessionRecord->archiveCurrentState(); + } + + RatchetingSession::initializeSession($sessionRecord->getSessionState(), + min($keyExchangeMessage->getMaxVersion(), CiphertextMessage::CURRENT_VERSION), + $parameters->create()); + + if ($sessionRecord->getSessionState()->getSessionVersion() >= 3 && !Curve::verifySignature( + $keyExchangeMessage->getIdentityKey()->getPublicKey(), + $keyExchangeMessage->getBaseKey()->serialize(), + $keyExchangeMessage->getBaseKeySignature())) { + throw new InvalidKeyException("Base key signature doesn't match!"); + } + + $this->sessionStore->storeSession($this->recipientId, $this->deviceId, $sessionRecord); + $this->identityKeyStore->saveIdentity($this->recipientId, $keyExchangeMessage->getIdentityKey()); + } + + public function processInitKeyExchangeMessage() + { + try { + $sequence = KeyHelper::getRandomSequence(65534) + 1; + $flags = KeyExchangeMessage::INITIATE_FLAG; + $baseKey = Curve::generateKeyPair(); + $ratchetKey = Curve::generateKeyPair(); + $identityKey = $this->identityKeyStore->getIdentityKeyPair(); + $baseKeySignature = Curve::calculateSignature($identityKey->getPrivateKey(), $baseKey->getPublicKey()->serialize()); + $sessionRecord = $this->sessionStore->loadSession($this->recipientId, $this->deviceId); + + $sessionRecord->getSessionState()->setPendingKeyExchange($sequence, $baseKey, $ratchetKey, $identityKey); + $this->sessionStore->storeSession($this->recipientId, $this->deviceId, $sessionRecord); + + return new KeyExchangeMessage(2, $sequence, $flags, $baseKey->getPublicKey(), $baseKeySignature, + $ratchetKey->getPublicKey(), $identityKey->getPublicKey()); + } catch (InvalidKeyException $ex) { + throw new Exception($ex->getMessage()); + } + } +} diff --git a/src/libaxolotl-php/SessionCipher.php b/src/libaxolotl-php/SessionCipher.php new file mode 100755 index 00000000..3a469f42 --- /dev/null +++ b/src/libaxolotl-php/SessionCipher.php @@ -0,0 +1,410 @@ +. + */ + +//namespace libaxolotl; + +require_once __DIR__.'/ecc/Curve.php'; +require_once __DIR__.'/ecc/ECKeyPair.php'; +require_once __DIR__.'/ecc/ECPublicKey.php'; +require_once __DIR__.'/protocol/CiphertextMessage.php'; +require_once __DIR__.'/protocol/PreKeyWhisperMessage.php'; +require_once __DIR__.'/protocol/WhisperMessage.php'; +require_once __DIR__.'/ratchet/ChainKey.php'; +require_once __DIR__.'/ratchet/MessageKeys.php'; +require_once __DIR__.'/ratchet/RootKey.php'; +require_once __DIR__.'/state/AxolotlStore.php'; +require_once __DIR__.'/state/IdentityKeyStore.php'; +require_once __DIR__.'/state/PreKeyStore.php'; +require_once __DIR__.'/state/SessionRecord.php'; +require_once __DIR__.'/state/SessionState.php'; +require_once __DIR__.'/state/SessionStore.php'; +require_once __DIR__.'/state/SignedPreKeyStore.php'; +require_once __DIR__.'/util/ByteUtil.php'; +require_once __DIR__.'/util/Pair.php'; + +//require_once "/state/SessionState/UnacknowledgedPreKeyMessageItems.php"; +class SessionCipher +{ + protected $sessionStore; + protected $preKeyStore; + protected $recepientId; + protected $deviceId; + protected $sessionBuilder; + + public function SessionCipher($sessionStore, $preKeyStore, $signedPreKeyStore, $identityKeyStore, $recepientId, $deviceId) + { + $this->sessionStore = $sessionStore; + $this->preKeyStore = $preKeyStore; + $this->recipientId = $recepientId; + $this->deviceId = $deviceId; + $this->sessionBuilder = new SessionBuilder($sessionStore, $preKeyStore, $signedPreKeyStore, + $identityKeyStore, $recepientId, $deviceId); + } + + public function encrypt($paddedMessage) + { + /* + :type paddedMessage: str + */ + + /*paddedMessage = bytearray(paddedMessage.encode() + if (sys.version_info >= (3,0) and not type(paddedMessage) in (bytes, bytearray)) + or type(paddedMessage) is unicode else paddedMessage)*/ + $sessionRecord = $this->sessionStore->loadSession($this->recipientId, $this->deviceId); + $sessionState = $sessionRecord->getSessionState(); + + $chainKey = $sessionState->getSenderChainKey(); + $messageKeys = $chainKey->getMessageKeys(); + + $senderEphemeral = $sessionState->getSenderRatchetKey(); + $previousCounter = $sessionState->getPreviousCounter(); + $sessionVersion = $sessionState->getSessionVersion(); + + $ciphertextBody = $this->getCiphertext($sessionVersion, $messageKeys, $paddedMessage); + $ciphertextMessage = new WhisperMessage($sessionVersion, $messageKeys->getMacKey(), + $senderEphemeral, $chainKey->getIndex(), + $previousCounter, $ciphertextBody, + $sessionState->getLocalIdentityKey(), + $sessionState->getRemoteIdentityKey()); + + if ($sessionState->hasUnacknowledgedPreKeyMessage()) { + $items = $sessionState->getUnacknowledgedPreKeyMessageItems(); + $localRegistrationid = $sessionState->getLocalRegistrationId(); + + $ciphertextMessage = new PreKeyWhisperMessage($sessionVersion, $localRegistrationid, $items->getPreKeyId(), + $items->getSignedPreKeyId(), $items->getBaseKey(), + $sessionState->getLocalIdentityKey(), + $ciphertextMessage); + } + $sessionState->setSenderChainKey($chainKey->getNextChainKey()); + $this->sessionStore->storeSession($this->recipientId, $this->deviceId, $sessionRecord); + + return $ciphertextMessage; + } + + public function decryptMsg($ciphertext) + { + /* + :type ciphertext: WhisperMessage + */ + if (!$this->sessionStore->containsSession($this->recipientId, $this->deviceId)) { + throw new NoSessionException('No session for: '.$this->recipientId.', '.$this->deviceId); + } + + $sessionRecord = $this->sessionStore->loadSession($this->recipientId, $this->deviceId); + $plaintext = $this->decryptWithSessionRecord($sessionRecord, $ciphertext); + + $this->sessionStore->storeSession($this->recipientId, $this->deviceId, $sessionRecord); + + /*if sys.version_info >= (3,0): + return plaintext.decode()*/ + return $plaintext; + } + + public function decryptPkmsg($ciphertext) + { + /* + :type ciphertext: PreKeyWhisperMessage + */ + $sessionRecord = $this->sessionStore->loadSession($this->recipientId, $this->deviceId); + $unsignedPreKeyId = $this->sessionBuilder->process($sessionRecord, $ciphertext); + + $plaintext = $this->decryptWithSessionRecord($sessionRecord, $ciphertext->getWhisperMessage()); + + //callback.handlePlaintext(plaintext); + $this->sessionStore->storeSession($this->recipientId, $this->deviceId, $sessionRecord); + + if ($unsignedPreKeyId != null) { + $this->preKeyStore->removePreKey($unsignedPreKeyId); + } + /* + if sys.version_info >= (3, 0): + return plaintext.decode() + */ + return $plaintext; + } + + public function decryptWithSessionRecord($sessionRecord, $cipherText) + { + /* + :type sessionRecord: SessionRecord + :type cipherText: WhisperMessage + */ + + $previousStates = $sessionRecord->getPreviousSessionStates(); + $exceptions = []; + try { + $sessionState = new SessionState($sessionRecord->getSessionState()); + $plaintext = $this->decryptWithSessionState($sessionState, $cipherText); + $sessionRecord->setState($sessionState); + + return $plaintext; + } catch (InvalidMessageException $e) { + echo $e->getMessage()."\n"; + $exceptions[] = $e; + } + + for ($i = 0; $i < count($previousStates); $i++) { + $previousState = $previousStates[$i]; + try { + $promotedState = new SessionState($previousState); + $plaintext = $this->decryptWithSessionState($promotedState, $cipherText); + $sessionRecord->removePreviousSessionStateAt($i); // del $previousStates[$i] + $sessionRecord->promoteState($promotedState); + + return $plaintext; + } catch (InvalidMessageException $e) { + echo $e->getMessage()."\n"; + $exceptions[] = $e; + } + } + + throw new InvalidMessageException('No valid sessions', $exceptions); + } + + public function decryptWithSessionState($sessionState, $ciphertextMessage) + { + if (!$sessionState->hasSenderChain()) { + throw new InvalidMessageException('Uninitialized session!'); + } + + if ($ciphertextMessage->getMessageVersion() != $sessionState->getSessionVersion()) { + throw new InvalidMessageException('Message version '.$ciphertextMessage->getMessageVersion().', but session version '.$sessionState->getSessionVersion()); + } + + $messageVersion = $ciphertextMessage->getMessageVersion(); + $theirEphemeral = $ciphertextMessage->getSenderRatchetKey(); + $counter = $ciphertextMessage->getCounter(); + $chainKey = $this->getOrCreateChainKey($sessionState, $theirEphemeral); + $messageKeys = $this->getOrCreateMessageKeys($sessionState, $theirEphemeral, + $chainKey, $counter); + + $ciphertextMessage->verifyMac($messageVersion, + $sessionState->getRemoteIdentityKey(), + $sessionState->getLocalIdentityKey(), + $messageKeys->getMacKey()); + + $plaintext = $this->getPlaintext($messageVersion, $messageKeys, $ciphertextMessage->getBody()); + $sessionState->clearUnacknowledgedPreKeyMessage(); + + return $plaintext; + } + + public function getOrCreateChainKey($sessionState, $ECPublicKey_theirEphemeral) + { + $theirEphemeral = $ECPublicKey_theirEphemeral; + if ($sessionState->hasReceiverChain($theirEphemeral)) { + return $sessionState->getReceiverChainKey($theirEphemeral); + } else { + $rootKey = $sessionState->getRootKey(); + + $ourEphemeral = $sessionState->getSenderRatchetKeyPair(); + $receiverChain = $rootKey->createChain($theirEphemeral, $ourEphemeral); + $ourNewEphemeral = Curve::generateKeyPair(); + $senderChain = $receiverChain[0]->createChain($theirEphemeral, $ourNewEphemeral); + + $sessionState->setRootKey($senderChain[0]); + $sessionState->addReceiverChain($theirEphemeral, $receiverChain[1]); + $sessionState->setPreviousCounter(max($sessionState->getSenderChainKey()->getIndex() - 1, 0)); + $sessionState->setSenderChain($ourNewEphemeral, $senderChain[1]); + + return $receiverChain[1]; + } + } + + public function getOrCreateMessageKeys($sessionState, $ECPublicKey_theirEphemeral, $chainKey, $counter) + { + $theirEphemeral = $ECPublicKey_theirEphemeral; + if ($chainKey->getIndex() > $counter) { + if ($sessionState->hasMessageKeys($theirEphemeral, $counter)) { + return $sessionState->removeMessageKeys($theirEphemeral, $counter); + } else { + throw new DuplicateMessageException('Received message '. + 'with old counter: '.$chainKey->getIndex().' '.$counter); + } + } + if ($counter - $chainKey->getIndex() > 2000) { + throw new InvalidMessageException('Over 2000 messages into the future!'); + } + + while ($chainKey->getIndex() < $counter) { + $messageKeys = $chainKey->getMessageKeys(); + $sessionState->setMessageKeys($theirEphemeral, $messageKeys); + $chainKey = $chainKey->getNextChainKey(); + } + $sessionState->setReceiverChainKey($theirEphemeral, $chainKey->getNextChainKey()); + + return $chainKey->getMessageKeys(); + } + + public function getCiphertext($version, $messageKeys, $plainText) + { + /* + :type version: int + :type messageKeys: MessageKeys + :type plainText: bytearray + */ + $cipher = null; + if ($version >= 3) { + $cipher = $this->getCipher($messageKeys->getCipherKey(), $messageKeys->getIv()); + } else { + $cipher = $this->getCipher_v2($messageKeys->getCipherKey(), $messageKeys->getCounter()); + } + + return $cipher->encrypt($plainText); + } + + public function getPlaintext($version, $messageKeys, $cipherText) + { + $cipher = null; + if ($version >= 3) { + $cipher = $this->getCipher($messageKeys->getCipherKey(), $messageKeys->getIv()); + } else { + $cipher = $this->getCipher_v2($messageKeys->getCipherKey(), $messageKeys->getCounter()); + } + + return $cipher->decrypt($cipherText); + } + + public function getCipher($key, $iv) + { + //Cipher.getInstance("AES/CBC/PKCS5Padding"); + //cipher = AES.new(key, AES.MODE_CBC, IV = iv) + //return cipher + return new AESCipher($key, $iv); + } + + public function getCipher_v2($key, $counter) + { + /* #AES/CTR/NoPadding + #counterbytes = struct.pack('>L', counter) + (b'\x00' * 12) + #counterint = struct.unpack(">L", counterbytes)[0] + #counterint = int.from_bytes(counterbytes, byteorder='big') + ctr=Counter.new(128, initial_value= counter) + + #cipher = AES.new(key, AES.MODE_CTR, counter=ctr) + ivBytes = bytearray(16) + ByteUtil.intToByteArray(ivBytes, 0, counter) + + cipher = AES.new(key, AES.MODE_CTR, IV = bytes(ivBytes), counter=ctr) + + return cipher;*/ + return new AESCipher($key, null, 2, new CryptoCounter(128, $counter)); + throw new Exception('To be implemented.'); + } +} +class CryptoCounter +{ + protected $size; + protected $val; + + public function CryptoCounter($size = 128, $init_val = 0) + { + $this->val = $init_val; + if (!in_array($size, [128, 192, 256])) { + throw new Exception('Counter size cannot be other than 128,192 or 256 bits'); + } + $this->size = $size / 8; + } + + public function Next() + { + $b = array_reverse(unpack('C*', pack('L', $this->val))); + //byte array to string + $ctr_str = implode(array_map('chr', $b)); + // create 16 byte IV from counter + $ctrVal = str_repeat("\x0", ($this->size - 4)).$ctr_str; + $this->val++; + + return $ctrVal; + } +} +class AESCipher +{ + protected $key; + protected $iv; + protected $version; + protected $counter; + + public function AESCipher($key, $iv, $version = 3, $counter = null) + { + $this->key = $key; + $this->iv = $iv; + $this->version = $version; + if ($this->version < 3 && $counter == null) { + throw new Exception('Counter is needed for version < 3'); + } + $this->counter = $counter; + } + + private function pad($s) + { + $BS = 16; + + return $s.str_repeat(chr($BS - (strlen($s) % $BS)), ($BS - (strlen($s) % $BS))); + } + + private function unpad($s, $diff = 0) + { + return substr($s, 0, -1 * (ord($s[strlen($s) - 1]) - $diff)); + } + + public function encrypt($raw) + { + // if sys.version_info >= (3,0): + // rawPadded = pad(raw.decode()).encode() + // else: + if ($this->version >= 3) { + $rawPadded = $this->pad($raw); + + return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $this->key, $rawPadded, MCRYPT_MODE_CBC, $this->iv); + } else { + return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $this->key, $raw, 'ctr', $this->counter->Next()); + } + } + + public function decrypt($enc) + { + if ($this->version >= 3) { + $result = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $this->key, $enc, MCRYPT_MODE_CBC, $this->iv); + + $unpaded = $this->unpad($result); + $last_unpadded = $unpaded[strlen($unpaded) - 1]; + $double_padding = substr($unpaded, -1 * (ord($last_unpadded) - 1)); + if (ord($last_unpadded) - 1 == strlen($double_padding)) { + $has_dp = true; + for ($x = 0; $x < strlen($double_padding); $x++) { + if ($double_padding[$x] != $last_unpadded) { + $has_dp = false; + break; + } + } + } else { + $has_dp = false; + } + if ($has_dp) { + $unpaded = $this->unpad($unpaded, 1); + } + + return $unpaded; + } else { + return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $this->key, $enc, 'ctr', $this->counter->Next()); + } + } +} diff --git a/src/libaxolotl-php/StaleKeyExchangeException.php b/src/libaxolotl-php/StaleKeyExchangeException.php new file mode 100755 index 00000000..5d5ba09a --- /dev/null +++ b/src/libaxolotl-php/StaleKeyExchangeException.php @@ -0,0 +1,5 @@ + php string == java byte array*/ + //foreach (range(0, (count($keyBytes) /*from: keyBytes.length*/ + 0)) as $_upto) $keyBytes[$_upto] = $bytes[$_upto - (0) + ($offset + 1)]; /* from: System.arraycopy(bytes, offset + 1, keyBytes, 0, keyBytes.length) */; + return new DjbECPublicKey($keyBytes); + default: + throw new InvalidKeyException('Bad key type: '.$type); + } + } + + public static function decodePrivatePoint($bytes) // [byte[] bytes] + { + return new DjbECPrivateKey($bytes); + } + + public static function calculateAgreement($publicKey, $privateKey) // [ECPublicKey publicKey, ECPrivateKey privateKey] + { + if (($publicKey->getType() != $privateKey->getType())) { + throw new InvalidKeyException('Public and private keys must be of the same type!'); + } + if (($publicKey->getType() == self::DJB_TYPE)) { + return curve25519_shared($privateKey->getPrivateKey(), $publicKey->getPublicKey()); + } else { + throw new InvalidKeyException('Unknown type: '.$publicKey->getType()); + } + } + + public static function verifySignature($signingKey, $message, $signature) // [ECPublicKey signingKey, byte[] message, byte[] signature] + { + if (($signingKey->getType() == self::DJB_TYPE)) { + return curve25519_verify($signingKey->getPublicKey(), $message, $signature) == 0; + } else { + throw new InvalidKeyException('Unknown type: '.$signingKey->getType()); + } + } + + public static function calculateSignature($signingKey, $message) // [ECPrivateKey signingKey, byte[] message] + { + if (($signingKey->getType() == self::DJB_TYPE)) { + return curve25519_sign(self::getSecureRandom(64), $signingKey->getPrivateKey(), $message); + } else { + throw new InvalidKeyException('Unknown type: '.$signingKey->getType()); + } + } + + protected static function getSecureRandom($len = 32) + { + $rand = openssl_random_pseudo_bytes($len, $strong); + if ($strong) { + return $rand; + } else { + throw new Exception('Cannot generate secure random bytes'); + } + } +} diff --git a/src/libaxolotl-php/ecc/DjbECPrivateKey.php b/src/libaxolotl-php/ecc/DjbECPrivateKey.php new file mode 100755 index 00000000..b3a102be --- /dev/null +++ b/src/libaxolotl-php/ecc/DjbECPrivateKey.php @@ -0,0 +1,26 @@ + php string now + + public function DjbECPrivateKey($privateKey) // [byte[] privateKey] + { + $this->privateKey = $privateKey; + } + + public function serialize() + { + return $this->privateKey; + } + + public function getType() + { + return Curve::DJB_TYPE; + } + + public function getPrivateKey() + { + return $this->privateKey; + } +} diff --git a/src/libaxolotl-php/ecc/DjbECPublicKey.php b/src/libaxolotl-php/ecc/DjbECPublicKey.php new file mode 100755 index 00000000..3f84a151 --- /dev/null +++ b/src/libaxolotl-php/ecc/DjbECPublicKey.php @@ -0,0 +1,62 @@ +publicKey = $publicKey; + } + + public function serialize() + { + return chr(Curve::DJB_TYPE).$this->publicKey; + } + + public function getType() + { + return Curve::DJB_TYPE; + } + + public function equals($other) // [Object other] + { + if (($other == null)) { + return false; + } + if (!($other instanceof self)) { + return false; + } + $that = $other; + + return $this->publicKey == $that->publicKey; + } + + public function compareTo($another) // [ECPublicKey another] + { + + //return new BigInteger($this->publicKey)::compareTo(new BigInteger(($another)::$publicKey)); + /*$current = unpack("H*",$this->publicKey); + $current= $current[1]; + //$current = intval($current[1],16); + $other = unpack("H*",$another->publicKey); + $other = $other[1]; + //$other = intval($other[1],16);*/ + for ($x = 0; $x < strlen($this->publicKey); $x++) { + if (ord($this->publicKey[$x]) > ord($another->publicKey[$x])) { + return 1; + } elseif (ord($this->publicKey[$x]) > ord($another->publicKey[$x])) { + return -1; + } + } + + return 0; + //return (($current > $other)?1: (($current == $other)?0:-1)); + } + + public function getPublicKey() + { + return $this->publicKey; + } +} diff --git a/src/libaxolotl-php/ecc/ECKeyPair.php b/src/libaxolotl-php/ecc/ECKeyPair.php new file mode 100755 index 00000000..3fde1cf6 --- /dev/null +++ b/src/libaxolotl-php/ecc/ECKeyPair.php @@ -0,0 +1,23 @@ +publicKey = $publicKey; + $this->privateKey = $privateKey; + } + + public function getPublicKey() + { + return $this->publicKey; + } + + public function getPrivateKey() + { + return $this->privateKey; + } +} diff --git a/src/libaxolotl-php/ecc/ECPrivateKey.php b/src/libaxolotl-php/ecc/ECPrivateKey.php new file mode 100755 index 00000000..e440030a --- /dev/null +++ b/src/libaxolotl-php/ecc/ECPrivateKey.php @@ -0,0 +1,8 @@ +senderKeyStore = $senderKeyStore; + $this->senderKeyId = $senderKeyId; + } + + public function encrypt($paddedPlaintext) + { + try { + $record = $this->senderKeyStore->loadSenderKey($this->senderKeyId); + $senderKeyState = $record->getSenderKeyState(); + $senderKey = $senderKeyState->getSenderChainKey()->getSenderMessageKey(); + $ciphertext = $this->getCipherText($senderKey->getIv(), $senderKey->getCipherKey(), $paddedPlaintext); + + $senderKeyMessage = new SenderKeyMessage($senderKeyState->getKeyId(), + $senderKey->getIteration(), + $ciphertext, + $senderKeyState->getSigningKeyPrivate()); + + $senderKeyState->setSenderChainKey($senderKeyState->getSenderChainKey()->getNext()); + $this->senderKeyStore->storeSenderKey($this->senderKeyId, $record); + + return $senderKeyMessage->serialize(); + } catch (InvalidKeyIdException $e) { + throw new NoSessionException($e->getMessage()); + } + } + + public function decrypt($senderKeyMessageBytes) + { + try { + $record = $this->senderKeyStore->loadSenderKey($this->senderKeyId); + $senderKeyMessage = new SenderKeyMessage(null, null, null, null, $senderKeyMessageBytes); + + $senderKeyState = $record->getSenderKeyState($senderKeyMessage->getKeyId()); + $senderKeyMessage->verifySignature($senderKeyState->getSigningKeyPublic()); + $senderKey = $this->getSenderKey($senderKeyState, $senderKeyMessage->getIteration()); + + $plaintext = $this->getPlainText($senderKey->getIv(), $senderKey->getCipherKey(), $senderKeyMessage->getCipherText()); + + $this->senderKeyStore->storeSenderKey($this->senderKeyId, $record); + + return $plaintext; + } catch (InvalidKeyException $e) { + throw new InvalidKeyException($e->getMessage()); + } + } + + public function getSenderKey($senderKeyState, $iteration) + { + $senderChainKey = $senderKeyState->getSenderChainKey(); + + if ($senderChainKey->getIteration() > $iteration) { + if ($senderKeyState->hasSenderMessageKey($iteration)) { + return $senderKeyState->removeSenderMessageKey($iteration); + } else { + throw new DuplicateMessageException('Received message with old counter: '. + $senderChainKey->getIteration().' '. + $iteration); + } + } + + if ($senderChainKey->getIteration() - $iteration > 2000) { + throw new InvalidMessageException('Over 2000 messages into the future!'); + } + + while ($senderChainKey->getIteration() < $iteration) { + $senderKeyState->addSenderMessageKey($senderChainKey->getSenderMessageKey()); + $senderChainKey = $senderChainKey->getNext(); + } + + $senderKeyState->setSenderChainKey($senderChainKey->getNext()); + + return $senderChainKey->getSenderMessageKey(); + } + + public function getPlainText($iv, $key, $ciphertext) + { + try { + $cipher = new AESCipher($key, $iv); + $plaintext = $cipher->decrypt($ciphertext); + + return $plaintext; + } catch (Exception $e) { + throw new InvalidMessageException($e->getMessage()); + } + } + + public function getCipherText($iv, $key, $plaintext) + { + $cipher = new AESCipher($key, $iv); + + return $cipher->encrypt($plaintext); + } +} diff --git a/src/libaxolotl-php/groups/GroupSessionBuilder.php b/src/libaxolotl-php/groups/GroupSessionBuilder.php new file mode 100755 index 00000000..296f9a37 --- /dev/null +++ b/src/libaxolotl-php/groups/GroupSessionBuilder.php @@ -0,0 +1,37 @@ +senderKeyStore = $senderKeyStore; + } + + public function processSender($sender, $senderKeyDistributionMessage) + { + $senderKeyRecord = $this->senderKeyStore->loadSenderKey($sender); + + $senderKeyRecord->addSenderKeyState($senderKeyDistributionMessage->getId(), + $senderKeyDistributionMessage->getIteration(), + $senderKeyDistributionMessage->getChainKey(), + $senderKeyDistributionMessage->getSignatureKey()); + $this->senderKeyStore->storeSenderKey($sender, $senderKeyRecord); + } + + public function process($groupId, $keyId, $iteration, $chainKey, $signatureKey) + { + $senderKeyRecord = $this->senderKeyStore->loadSenderKey($groupId); + + $senderKeyRecord->setSenderKeyState($keyId, $iteration, $chainKey, $signatureKey); + + $this->senderKeyStore->storeSenderKey($groupId, $senderKeyRecord); + + return new SenderKeyDistributionMessage($keyId, $iteration, $chainKey, $signatureKey->getPublicKey()); + } +} diff --git a/src/libaxolotl-php/groups/ratchet/SenderChainKey.php b/src/libaxolotl-php/groups/ratchet/SenderChainKey.php new file mode 100755 index 00000000..440b8741 --- /dev/null +++ b/src/libaxolotl-php/groups/ratchet/SenderChainKey.php @@ -0,0 +1,43 @@ +iteration = $iteration; + $this->chainKey = $chainKey; + } + + public function getIteration() + { + return $this->iteration; + } + + public function getSenderMessageKey() + { + return new SenderMessageKey($this->iteration, $this->getDerivative(self::MESSAGE_KEY_SEED, $this->chainKey)); + } + + public function getNext() + { + return new self($this->iteration + 1, $this->getDerivative(self::CHAIN_KEY_SEED, $this->chainKey)); + } + + public function getSeed() + { + return $this->chainKey; + } + + public function getDerivative($seed, $key) + { + $mac = hash_init('sha256', HASH_HMAC, $key); + hash_update($mac, $seed); + + return hash_final($mac, true); + } + } diff --git a/src/libaxolotl-php/groups/ratchet/SenderMessageKey.php b/src/libaxolotl-php/groups/ratchet/SenderMessageKey.php new file mode 100755 index 00000000..3f0e6f70 --- /dev/null +++ b/src/libaxolotl-php/groups/ratchet/SenderMessageKey.php @@ -0,0 +1,43 @@ +deriveSecrets($seed, 'WhisperGroup', 48); + /* match: 21c8b6ca */ + $parts = ByteUtil::split($derivative, 16, 32); + $this->iteration = $iteration; + $this->seed = $seed; + $this->iv = $parts[0]; + $this->cipherKey = $parts[1]; + } + + public function getIteration() + { + return $this->iteration; + } + + public function getIv() + { + return $this->iv; + } + + public function getCipherKey() + { + return $this->cipherKey; + } + + public function getSeed() + { + return $this->seed; + } +} diff --git a/src/libaxolotl-php/groups/state/SenderKeyRecord.php b/src/libaxolotl-php/groups/state/SenderKeyRecord.php new file mode 100644 index 00000000..77f6ad77 --- /dev/null +++ b/src/libaxolotl-php/groups/state/SenderKeyRecord.php @@ -0,0 +1,66 @@ +senderKeyStates = []; + + if ($serialized != null) { + $senderKeyRecordStructure = new TextSecure_SenderKeyRecordStructure(); + + $senderKeyRecordStructure->parseFromString($serialized); + + foreach ($senderKeyRecordStructure->getSenderKeyStates() as $structure) { + $this->senderKeyStates[] = new SenderKeyState(null, null, null, null, null, null, $structure); + } + } + } + + public function getSenderKeyState($keyId = null) + { + if (is_null($keyId)) { + if (count($this->senderKeyStates) > 0) { + return $this->senderKeyStates[0]; + } else { + throw new InvalidKeyIdException('No key state in record'); + } + } else { + foreach ($this->senderKeyStates as $state) { + if ($state->getKeyId() == $keyId) { + return $state; + } + } + throw new InvalidKeyIdException("No keys for: $keyId"); + } + } + + public function addSenderKeyState($id, $iteration, $chainKey, $signatureKey) + { + $this->senderKeyStates[] = new SenderKeyState($id, $iteration, $chainKey, $signatureKey); + } + + public function setSenderKeyState($id, $iteration, $chainKey, $signatureKey) + { + unset($this->senderKeyStates); + $this->senderKeyStates = []; + $this->senderKeyStates[] = new SenderKeyState($id, $iteration, $chainKey, null, null, $signatureKey); + } + + public function serialize() + { + $recordStructure = new TextSecure_SenderKeyRecordStructure(); + + foreach ($this->senderKeyStates as $senderKeyState) { + $recordStructure->appendSenderKeyStates($senderKeyState->getStructure()); + } + + return $recordStructure->serializeToString(); + } +} diff --git a/src/libaxolotl-php/groups/state/SenderKeyState.php b/src/libaxolotl-php/groups/state/SenderKeyState.php new file mode 100644 index 00000000..b2bd0a5d --- /dev/null +++ b/src/libaxolotl-php/groups/state/SenderKeyState.php @@ -0,0 +1,132 @@ +senderKeyStateStructure = $senderKeyStateStructure; + } else { + if ($signatureKeyPair != null) { + $signatureKeyPublic = $signatureKeyPair->getPublicKey(); + $signatureKeyPrivate = $signatureKeyPair->getPrivateKey(); + } + + $this->senderKeyStateStructure = new Textsecure_SenderKeyStateStructure(); + $senderChainKeyStructure = $this->senderKeyStateStructure->getSenderChainKey(); + if ($senderChainKeyStructure == null) { + $senderChainKeyStructure = new Textsecure_SenderKeyStateStructure_SenderChainKey(); + $this->senderKeyStateStructure->setSenderChainKey($senderChainKeyStructure); + } + + $this->senderKeyStateStructure->getSenderChainKey()->setIteration($iteration); + $this->senderKeyStateStructure->getSenderChainKey()->setSeed($chainKey); + + $signingKeyStructure = $this->senderKeyStateStructure->getSenderSigningKey(); + if ($signingKeyStructure == null) { + $signingKeyStructure = new Textsecure_SenderKeyStateStructure_SenderSigningKey(); + $this->senderKeyStateStructure->setSenderSigningKey($signingKeyStructure); + } + $this->senderKeyStateStructure->getSenderSigningKey()->setPublic($signatureKeyPublic->serialize()); + + if ($signatureKeyPrivate) { + $this->senderKeyStateStructure->getSenderSigningKey()->setPrivate($signatureKeyPrivate->serialize()); + } + + $this->senderKeyStateStructure->setSenderKeyId($id); + $this->senderChainKey = $senderChainKeyStructure; + $this->senderKeyStateStructure->setSenderSigningKey($signingKeyStructure); + } + } + + public function getKeyId() + { + return $this->senderKeyStateStructure->getSenderKeyId(); + } + + public function getSenderChainKey() + { + return new SenderChainKey($this->senderKeyStateStructure->getSenderChainKey()->getIteration(), + $this->senderKeyStateStructure->getSenderChainKey()->getSeed()); + } + + public function setSenderChainKey($chainKey) + { + $this->senderKeyStateStructure->getSenderChainKey()->setIteration($chainKey->getIteration()); + $this->senderKeyStateStructure->getSenderChainKey()->setSeed($chainKey->getSeed()); + } + + public function getSigningKeyPublic() + { + return Curve::decodePoint($this->senderKeyStateStructure->getSenderSigningKey()->getPublic(), 0); + } + + public function getSigningKeyPrivate() + { + return Curve::decodePrivatePoint($this->senderKeyStateStructure->getSenderSigningKey()->getPrivate()); + } + + public function hasSenderMessageKey($iteration) + { + foreach ($this->senderKeyStateStructure->getSenderMessageKeys() as $senderMessageKey) { + if ($senderMessageKey->getIteration() == $iteration) { + return true; + } + } + + return false; + } + + public function addSenderMessageKey($senderMessageKey) + { + $smk = new Textsecure_SenderKeyStateStructure_SenderMessageKey(); + $smk->setIteration($senderMessageKey->getIteration()); + $smk->setSeed($senderMessageKey->getSeed()); + $this->senderKeyStateStructure->appendSenderMessageKeys($smk); + } + + public function removeSenderMessageKey($iteration) + { + $keys = $this->senderKeyStateStructure->getSenderMessageKeys(); + $result = null; + + for ($i = 0; $i < count($keys); $i++) { + $senderMessageKey = $keys[$i]; + if ($senderMessageKey->getIteration() == $iteration) { + $result = $senderMessageKey; + unset($keys[$i]); + break; + } + } + $this->senderKeyStateStructure->clearSenderMessageKeys(); + foreach ($keys as $key) { + $this->senderKeyStateStructure->appendSenderMessageKeys($key); + } + + if (!is_null($result)) { + return new SenderMessageKey($result->getIteration(), $result->getSeed()); + } + } + + public function getStructure() + { + return $this->senderKeyStateStructure; + } +} diff --git a/src/libaxolotl-php/groups/state/SenderKeyStore.php b/src/libaxolotl-php/groups/state/SenderKeyStore.php new file mode 100755 index 00000000..46cac800 --- /dev/null +++ b/src/libaxolotl-php/groups/state/SenderKeyStore.php @@ -0,0 +1,12 @@ +cipherKey = $keys[0]; //AES + $this->macKey = $keys[1]; //sha256 + $this->iv = $keys[2]; + } + + public function getCipherKey() + { + return $this->cipherKey; + } + + public function getMacKey() + { + return $this->macKey; + } + + public function getIv() + { + return $this->iv; + } +} diff --git a/src/libaxolotl-php/kdf/DerivedRootSecrets.php b/src/libaxolotl-php/kdf/DerivedRootSecrets.php new file mode 100755 index 00000000..1d850076 --- /dev/null +++ b/src/libaxolotl-php/kdf/DerivedRootSecrets.php @@ -0,0 +1,26 @@ +rootKey = $keys[0]; + $this->chainKey = $keys[1]; + } + + public function getRootKey() + { + return $this->rootKey; + } + + public function getChainKey() + { + return $this->chainKey; + } +} diff --git a/src/libaxolotl-php/kdf/HKDF.php b/src/libaxolotl-php/kdf/HKDF.php new file mode 100755 index 00000000..8b25a20e --- /dev/null +++ b/src/libaxolotl-php/kdf/HKDF.php @@ -0,0 +1,58 @@ +extract($salt, $inputKey); + + return $this->expand($prk, $info, $outputLength); + } + + public function extract($salt, $inputKey) + { + $mac = hash_init('sha256', HASH_HMAC, $salt); + hash_update($mac, $inputKey); + + return hash_final($mac, true); + } + + public function expand($prk, $info, $outputSize) + { + $iterations = (int) ceil(floatval($outputSize) / floatval(self::HASH_OUTPUT_SIZE)); + $remainingBytes = $outputSize; + $mixin = ''; + $result = ''; + for ($i = $this->getIterationStartOffset(); $i < $iterations + $this->getIterationStartOffset(); $i++) { + $mac = hash_init('sha256', HASH_HMAC, $prk); + hash_update($mac, $mixin); + if ($info != null) { + hash_update($mac, $info); + } + $updateChr = chr($i % 256); + hash_update($mac, $updateChr); + $stepResult = hash_final($mac, true); + $stepSize = min($remainingBytes, strlen($stepResult)); + $result .= substr($stepResult, 0, $stepSize); + $mixin = $stepResult; + $remainingBytes -= $stepSize; + } + + return $result; + } + } diff --git a/src/libaxolotl-php/kdf/HKDFv2.php b/src/libaxolotl-php/kdf/HKDFv2.php new file mode 100755 index 00000000..d73227ee --- /dev/null +++ b/src/libaxolotl-php/kdf/HKDFv2.php @@ -0,0 +1,10 @@ +getTrace(); + } else { + return ''; + } + } + + //old function name log + + public static function writeLog($priority, $tag, $msg) // [int priority, String tag, String msg] + { + $logger = AxolotlLoggerProvider::getProvider(); + if (($logger != null)) { + $logger->log($priority, $tag, $msg); + } + } +} diff --git a/src/libaxolotl-php/protobuf/LocalStorageProtocol.proto b/src/libaxolotl-php/protobuf/LocalStorageProtocol.proto new file mode 100755 index 00000000..fb7c707b --- /dev/null +++ b/src/libaxolotl-php/protobuf/LocalStorageProtocol.proto @@ -0,0 +1,112 @@ +package textsecure; + +option java_package = "org.whispersystems.libaxolotl.state"; +option java_outer_classname = "StorageProtos"; + +message SessionStructure { + message Chain { + optional bytes senderRatchetKey = 1; + optional bytes senderRatchetKeyPrivate = 2; + + message ChainKey { + optional uint32 index = 1; + optional bytes key = 2; + } + + optional ChainKey chainKey = 3; + + message MessageKey { + optional uint32 index = 1; + optional bytes cipherKey = 2; + optional bytes macKey = 3; + optional bytes iv = 4; + } + + repeated MessageKey messageKeys = 4; + } + + message PendingKeyExchange { + optional uint32 sequence = 1; + optional bytes localBaseKey = 2; + optional bytes localBaseKeyPrivate = 3; + optional bytes localRatchetKey = 4; + optional bytes localRatchetKeyPrivate = 5; + optional bytes localIdentityKey = 7; + optional bytes localIdentityKeyPrivate = 8; + } + + message PendingPreKey { + optional uint32 preKeyId = 1; + optional int32 signedPreKeyId = 3; + optional bytes baseKey = 2; + } + + optional uint32 sessionVersion = 1; + optional bytes localIdentityPublic = 2; + optional bytes remoteIdentityPublic = 3; + + optional bytes rootKey = 4; + optional uint32 previousCounter = 5; + + optional Chain senderChain = 6; + repeated Chain receiverChains = 7; + + optional PendingKeyExchange pendingKeyExchange = 8; + optional PendingPreKey pendingPreKey = 9; + + optional uint32 remoteRegistrationId = 10; + optional uint32 localRegistrationId = 11; + + optional bool needsRefresh = 12; + optional bytes aliceBaseKey = 13; +} + +message RecordStructure { + optional SessionStructure currentSession = 1; + repeated SessionStructure previousSessions = 2; +} + +message PreKeyRecordStructure { + optional uint32 id = 1; + optional bytes publicKey = 2; + optional bytes privateKey = 3; +} + +message SignedPreKeyRecordStructure { + optional uint32 id = 1; + optional bytes publicKey = 2; + optional bytes privateKey = 3; + optional bytes signature = 4; + optional fixed64 timestamp = 5; +} + +message IdentityKeyPairStructure { + optional bytes publicKey = 1; + optional bytes privateKey = 2; +} + +message SenderKeyStateStructure { + message SenderChainKey { + optional uint32 iteration = 1; + optional bytes seed = 2; + } + + message SenderMessageKey { + optional uint32 iteration = 1; + optional bytes seed = 2; + } + + message SenderSigningKey { + optional bytes public = 1; + optional bytes private = 2; + } + + optional uint32 senderKeyId = 1; + optional SenderChainKey senderChainKey = 2; + optional SenderSigningKey senderSigningKey = 3; + repeated SenderMessageKey senderMessageKeys = 4; +} + +message SenderKeyRecordStructure { + repeated SenderKeyStateStructure senderKeyStates = 1; +} \ No newline at end of file diff --git a/src/libaxolotl-php/protobuf/WhisperTextProtocol.proto b/src/libaxolotl-php/protobuf/WhisperTextProtocol.proto new file mode 100755 index 00000000..7b4fe6ea --- /dev/null +++ b/src/libaxolotl-php/protobuf/WhisperTextProtocol.proto @@ -0,0 +1,41 @@ +package textsecure; + +option java_package = "org.whispersystems.libaxolotl.protocol"; +option java_outer_classname = "WhisperProtos"; + +message WhisperMessage { + optional bytes ratchetKey = 1; + optional uint32 counter = 2; + optional uint32 previousCounter = 3; + optional bytes ciphertext = 4; +} + +message PreKeyWhisperMessage { + optional uint32 registrationId = 5; + optional uint32 preKeyId = 1; + optional uint32 signedPreKeyId = 6; + optional bytes baseKey = 2; + optional bytes identityKey = 3; + optional bytes message = 4; // WhisperMessage +} + +message KeyExchangeMessage { + optional uint32 id = 1; + optional bytes baseKey = 2; + optional bytes ratchetKey = 3; + optional bytes identityKey = 4; + optional bytes baseKeySignature = 5; +} + +message SenderKeyMessage { + optional uint32 id = 1; + optional uint32 iteration = 2; + optional bytes ciphertext = 3; +} + +message SenderKeyDistributionMessage { + optional uint32 id = 1; + optional uint32 iteration = 2; + optional bytes chainKey = 3; + optional bytes signingKey = 4; +} \ No newline at end of file diff --git a/src/libaxolotl-php/protocol/CiphertextMessage.php b/src/libaxolotl-php/protocol/CiphertextMessage.php new file mode 100755 index 00000000..3c24b046 --- /dev/null +++ b/src/libaxolotl-php/protocol/CiphertextMessage.php @@ -0,0 +1,16 @@ +supportedVersion = CiphertextMessage::CURRENT_VERSION; + $this->version = $messageVersion; + $this->sequence = $sequence; + $this->flags = $flags; + $this->baseKey = $baseKey; + $this->baseKeySignature = $baseKeySignature; + $this->ratchetKey = $ratchetKey; + $this->identityKey = $identityKey; + + $version = ByteUtil::intsToByteHighAndLow($this->version, $this->supportedVersion); + $keyExchangeMessage = new Textsecure_KeyExchangeMessage(); + $keyExchangeMessage->setId(($this->sequence << 5) | $this->flags); + $keyExchangeMessage->setBaseKey($baseKey->serialize()); + $keyExchangeMessage->setRatchetKey($ratchetKey->serialize()); + $keyExchangeMessage->setIdentityKey($identityKey->serialize()); + + if ($messageVersion >= 3) { + $keyExchangeMessage->setBaseKeySignature($baseKeySignature); + } + + $this->serialized = ByteUtil::combine([chr((int) $version), $keyExchangeMessage->serializeToString()]); + } else { + try { + $parts = ByteUtil::split($serialized, 1, strlen($serialized) - 1); + $this->version = ByteUtil::highBitsToInt(ord($parts[0][0])); + $this->supportedVersion = ByteUtil::lowBitsToInt(ord($parts[0][0])); + if ($this->version <= CiphertextMessage::UNSUPPORTED_VERSION) { + throw new LegacyMessageException('Unsupported legacy version: '.$this->version); + } + if ($this->version > CiphertextMessage::CURRENT_VERSION) { + throw new InvalidVersionException('Unkown version: '.$this->version); + } + $message = new Textsecure_KeyExchangeMessage(); + $message->parseFromString($parts[1]); + + if ($message->getId() == null || $message->getBaseKey() == null || + $message->getRatchetKey() == null || $message->getIdentityKey() == null || + ($this->version >= 3 && $message->getBaseKeySignature() == null)) { + throw new InvalidMessageException('Some required fields are missing!'); + } + + $this->sequence = $message->getId() >> 5; + $this->flags = $message->getId() & 0x1f; + $this->serialized = $serialized; + $this->baseKey = Curve::decodePoint($message->getBaseKey(), 0); + $this->baseKeySignature = $message->getBaseKeySignature(); + $this->ratchetKey = Curve::decodePoint($message->getRatchetKey(), 0); + $this->identityKey = new IdentityKey($message->getIdentityKey(), 0); + } catch (InvalidKeyException $ex) { + throw new InvalidMessageException($ex->getMessage()); + } + } + } + + public function getVersion() + { + return $this->version; + } + + public function getBaseKey() + { + return $this->baseKey; + } + + public function getBaseKeySignature() + { + return $this->baseKeySignature; + } + + public function getRatchetKey() + { + return $this->ratchetKey; + } + + public function getIdentityKey() + { + return $this->identityKey; + } + + public function hasIdentityKey() + { + return true; + } + + public function getMaxVersion() + { + return $this->supportedVersion; + } + + public function isResponse() + { + return ($this->flags & self::RESPONSE_FLAG) != 0; + } + + public function isInitiate() + { + return ($this->flags & self::INITIATE_FLAG) != 0; + } + + public function isResponseForSimultaneousInitiate() + { + return ($this->flags & self::SIMULTANEOUS_INITIATE_FLAG) != 0; + } + + public function getFlags() + { + return $this->flags; + } + + public function getSequence() + { + return $this->sequence; + } + + public function serialize() + { + return $this->serialized; + } + } diff --git a/src/libaxolotl-php/protocol/PreKeyWhisperMessage.php b/src/libaxolotl-php/protocol/PreKeyWhisperMessage.php new file mode 100755 index 00000000..64729121 --- /dev/null +++ b/src/libaxolotl-php/protocol/PreKeyWhisperMessage.php @@ -0,0 +1,134 @@ +version = ByteUtil::highBitsToInt($serialized[0]); + if ($this->version > self::CURRENT_VERSION) { + throw new InvalidVersionException('Unknown version '.$this->version); + } + $preKeyWhisperMessage = new Textsecure_PreKeyWhisperMessage(); + + $preKeyWhisperMessage->parseFromString(substr($serialized, 1)); + if (($this->version == 2 && $preKeyWhisperMessage->getPreKeyId() == null) || + ($this->version == 3 && $preKeyWhisperMessage->getSignedPreKeyId() == null) || + $preKeyWhisperMessage->getBaseKey() == null || + $preKeyWhisperMessage->getIdentityKey() == null || + $preKeyWhisperMessage->getMessage() == null) { + throw new InvalidMessageException('Incomplete message'); + } + + $this->serialized = $serialized; + $this->registrationId = $preKeyWhisperMessage->getRegistrationId(); + $this->preKeyId = $preKeyWhisperMessage->getPreKeyId(); + $this->signedPreKeyId = $preKeyWhisperMessage->getSignedPreKeyId(); + $this->baseKey = Curve::decodePoint((string) $preKeyWhisperMessage->getBaseKey(), 0); + $this->identityKey = new IdentityKey(Curve::decodePoint((string) $preKeyWhisperMessage->getIdentityKey(), 0)); + $this->message = new WhisperMessage(null, + null, + null, + null, + null, + null, + null, + null, + $preKeyWhisperMessage->getMessage()); + } else { + try { + $this->version = $messageVersion; + $this->registrationId = $registrationId; + $this->preKeyId = $preKeyId; + $this->signedPreKeyId = $signedPreKeyId; + $this->baseKey = $ecPublicBaseKey; + $this->identityKey = $identityKey; + $this->message = $whisperMessage; + + $builder = new Textsecure_PreKeyWhisperMessage(); + $builder->setSignedPreKeyId($this->signedPreKeyId); + $builder->setBaseKey($this->baseKey->serialize()); + $builder->setIdentityKey($this->identityKey->serialize()); + $builder->setMessage($whisperMessage->serialize()); + $builder->setRegistrationId($this->registrationId); + if ($preKeyId != null) { + $builder->setPreKeyId($preKeyId); + } + $versionBytes = ByteUtil::intsToByteHighAndLow($this->version, self::CURRENT_VERSION); + $messageBytes = $builder->serializeToString(); + $this->serialized = ByteUtil::combine([chr($versionBytes), $messageBytes]); + } catch (Exception $ex) { + throw new InvalidMessageException($ex->getMessage().' - '.$ex->getLine().' - '.$ex->getFile()); + } + } + } + + public function getMessageVersion() + { + return $this->version; + } + + public function getIdentityKey() + { + return $this->identityKey; + } + + public function getRegistrationId() + { + return $this->registrationId; + } + + public function getPreKeyId() + { + return $this->preKeyId; + } + + public function getSignedPreKeyId() + { + return $this->signedPreKeyId; + } + + public function getBaseKey() + { + return $this->baseKey; + } + + public function getWhisperMessage() + { + return $this->message; + } + + public function serialize() + { + return $this->serialized; + } + + public function getType() + { + return self::PREKEY_TYPE; + } + } diff --git a/src/libaxolotl-php/protocol/SenderKeyDistributionMessage.php b/src/libaxolotl-php/protocol/SenderKeyDistributionMessage.php new file mode 100755 index 00000000..994c7c44 --- /dev/null +++ b/src/libaxolotl-php/protocol/SenderKeyDistributionMessage.php @@ -0,0 +1,92 @@ +id = $id; + $this->iteration = $iteration; + $this->chainKey = $chainKey; + $this->signatureKey = $signatureKey; + + $proto_skdm = new Textsecure_SenderKeyDistributionMessage(); + $proto_skdm->setId($id); + $proto_skdm->setIteration($iteration); + $proto_skdm->setChainKey((string) $chainKey); + $proto_skdm->setSigningKey((string) ($signatureKey->serialize())); + $this->serialized = chr($version).$proto_skdm->serializeToString(); + } else { + $parts = ByteUtil::split($serialized, 1, strlen($serialized) - 1); + $version = ord($parts[0][0]); + $message = $parts[1]; + if (ByteUtil::highBitsToInt($version) < self::CURRENT_VERSION) { + throw new LegacyMessageException('Legacy message: ' + ByteUtil::highBitsToInt($version)); + } + if (ByteUtil::highBitsToInt($version) > self::CURRENT_VERSION) { + throw new InvalidMessageException('Unknown version: ' + ByteUtil::highBitsToInt($version)); + } + $proto_skdm = new Textsecure_SenderKeyDistributionMessage(); + try { + $proto_skdm->parseFromString($message); + } catch (Exception $ex) { + throw new InvalidMessageException('Incomplete message.'); + } + if ($proto_skdm->getId() === null + || $proto_skdm->getIteration() === null + || $proto_skdm->getChainKey() === null + || $proto_skdm->getSigningKey() === null) { + throw new InvalidMessageException('Incomplete message.'); + } + $this->serialized = $serialized; + $this->id = $proto_skdm->getId(); + $this->iteration = $proto_skdm->getIteration(); + $this->chainKey = $proto_skdm->getChainKey(); + $this->signatureKey = Curve::decodePoint($proto_skdm->getSigningKey(), 0); + } + } + + public function serialize() + { + return $this->serialized; + } + + public function getType() + { + return self::SENDERKEY_DISTRIBUTION_TYPE; + } + + public function getIteration() + { + return $this->iteration; + } + + public function getChainKey() + { + return $this->chainKey; + } + + public function getSignatureKey() + { + return $this->signatureKey; + } + + public function getId() + { + return $this->id; + } +} diff --git a/src/libaxolotl-php/protocol/SenderKeyMessage.php b/src/libaxolotl-php/protocol/SenderKeyMessage.php new file mode 100755 index 00000000..7eceabaa --- /dev/null +++ b/src/libaxolotl-php/protocol/SenderKeyMessage.php @@ -0,0 +1,118 @@ +setId($keyId); + $proto_message->setIteration($iteration); + $proto_message->setCiphertext($ciphertext); + + $message = $proto_message->serializeToString(); + + $signature = $this->getSignature($signatureKey, ByteUtil::combine([chr((int) $version), $message])); + + $this->serialized = ByteUtil::combine([chr((int) $version), $message, $signature]); + $this->messageVersion = self::CURRENT_VERSION; + $this->keyId = $keyId; + $this->iteration = $iteration; + $this->ciphertext = $ciphertext; + } else { + try { + $messageParts = ByteUtil::split($serialized, 1, strlen($serialized) - 1 - self::SIGNATURE_LENGTH, + self::SIGNATURE_LENGTH); + + $version = ord($messageParts[0][0]); + $message = $messageParts[1]; + $signature = $messageParts[2]; + if (ByteUtil::highBitsToInt($version) < 3) { + throw new LegacyMessageException('Legacy message: '.ByteUtil::highBitsToInt($version)); + } + + if (ByteUtil::highBitsToInt($version) > self::CURRENT_VERSION) { + throw new InvalidMessageException('Unknown version: '.ByteUtil::highBitsToInt($version)); + } + + $proto_message = new Textsecure_SenderKeyMessage(); + try { + $proto_message->parseFromString($message); + } catch (Exception $ex) { + throw new InvalidMessageException('Incomplete message'); + } + + if ($proto_message->getId() === null || $proto_message->getIteration() === null || $proto_message->getCiphertext() == null) { + throw new InvalidMessageException('Incomplete message'); + } + + $this->serialized = $serialized; + $this->messageVersion = ByteUtil::highBitsToInt($version); + $this->keyId = $proto_message->getId(); + $this->iteration = $proto_message->getIteration(); + $this->ciphertext = $proto_message->getCiphertext(); + } catch (Exception $ex) { + throw new InvalidMessageException($ex->getMessage()); + } + } + } + + public function getKeyId() + { + return $this->keyId; + } + + public function getIteration() + { + return $this->iteration; + } + + public function getCiphertext() + { + return $this->ciphertext; + } + + public function verifySignature($signatureKey) + { + try { + $parts = ByteUtil::split($this->serialized, strlen($this->serialized) - self::SIGNATURE_LENGTH, self::SIGNATURE_LENGTH); + if (!Curve::verifySignature($signatureKey, $parts[0], $parts[1])) { + throw new InvalidMessageException('Invalid signature!'); + } + } catch (InvalidKeyException $ex) { + throw new InvalidMessageException($ex->getMessage()); + } + } + + private function getSignature($signatureKey, $serialized) + { + try { + return Curve::calculateSignature($signatureKey, $serialized); + } catch (InvalidKeyException $ex) { + throw new Exception($ex->getMessage()); + } + } + + public function serialize() + { + return $this->serialized; + } + + public function getType() + { + return self::SENDERKEY_TYPE; + } + } diff --git a/src/libaxolotl-php/protocol/WhisperMessage.php b/src/libaxolotl-php/protocol/WhisperMessage.php new file mode 100755 index 00000000..36025ab0 --- /dev/null +++ b/src/libaxolotl-php/protocol/WhisperMessage.php @@ -0,0 +1,138 @@ +setRatchetKey((string) ($senderRatchetKey->serialize())); + $proto_message->setCounter($counter); + $proto_message->setPreviousCounter($previousCounter); + $proto_message->setCiphertext($cipherText); + $message = $proto_message->serializeToString(); + + $mac = $this->getMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey, ByteUtil::combine([chr((int) $version), $message])); + + $this->serialized = ByteUtil::combine([chr((int) $version), $message, $mac]); + $this->senderRatchetKey = $senderRatchetKey; + $this->counter = $counter; + $this->previousCounter = $previousCounter; + $this->cipherText = $cipherText; + $this->messageVersion = $messageVersion; + } else { + try { + $messageParts = ByteUtil::split($serialized, 1, strlen($serialized) - 1 - self::MAC_LENGTH, + self::MAC_LENGTH); + + $version = ord($messageParts[0][0]); + $message = $messageParts[1]; + $mac = $messageParts[2]; + if (ByteUtil::highBitsToInt($version) <= self::UNSUPPORTED_VERSION) { + throw new LegacyMessageException('Legacy message '.ByteUtil::highBitsToInt($version)); + } + if (ByteUtil::highBitsToInt($version) > self::CURRENT_VERSION) { + throw new InvalidMessageException('Unknown version: '.ByteUtil::highBitsToInt($version)); + } + + $proto_message = new Textsecure_WhisperMessage(); + try { + $proto_message->parseFromString($message); + } catch (Exception $ex) { + throw new InvalidMessageException('Incomplete message.'); + } + if ($proto_message->getCiphertext() === null || $proto_message->getCounter() === null || $proto_message->getRatchetKey() == null) { + throw new InvalidMessageException('Incomplete message.'); + } + $this->serialized = $serialized; + $this->senderRatchetKey = Curve::decodePoint($proto_message->getRatchetKey(), 0); + $this->messageVersion = ByteUtil::highBitsToInt($version); + $this->counter = $proto_message->getCounter(); + $this->previousCounter = $proto_message->getPreviousCounter(); + $this->cipherText = $proto_message->getCiphertext(); + } catch (Exception $ex) { + throw new InvalidMessageException($ex->getMessage()); + } + } + } + + public function getSenderRatchetKey() + { + return $this->senderRatchetKey; + } + + public function getMessageVersion() + { + return $this->messageVersion; + } + + public function getCounter() + { + return $this->counter; + } + + public function getBody() + { + return $this->cipherText; + } + + public function serialize() + { + return $this->serialized; + } + + public function getType() + { + return self::WHISPER_TYPE; + } + + public function isLegacy($message) + { + return $message != null && strlen($message) >= 1 && ByteUtil::highBitsToInt($message[0]) <= self::UNSUPPORTED_VERSION; + } + + public function verifyMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey) + { + $parts = ByteUtil::split($this->serialized, strlen($this->serialized) - self::MAC_LENGTH, self::MAC_LENGTH); + $ourMac = $this->getMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey, $parts[0]); + $theirMac = $parts[1]; + if (strcmp($ourMac, $theirMac) != 0) { + throw new InvalidMessageException('Bad Mac!'); + } + } + + private function getMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey, $serialized) + { + $mac = hash_init('sha256', HASH_HMAC, $macKey); + if ($messageVersion >= 3) { + hash_update($mac, $senderIdentityKey->getPublicKey()->serialize()); + hash_update($mac, $receiverIdentityKey->getPublicKey()->serialize()); + } + hash_update($mac, $serialized); + $result = hash_final($mac, true); + + return ByteUtil::trim($result, self::MAC_LENGTH); + } + } diff --git a/src/libaxolotl-php/protocol/pb_proto_WhisperTextProtocol.php b/src/libaxolotl-php/protocol/pb_proto_WhisperTextProtocol.php new file mode 100755 index 00000000..cc95fe78 --- /dev/null +++ b/src/libaxolotl-php/protocol/pb_proto_WhisperTextProtocol.php @@ -0,0 +1,849 @@ + [ + 'name' => 'ratchetKey', + 'required' => false, + 'type' => 7, + ], + self::COUNTER => [ + 'name' => 'counter', + 'required' => false, + 'type' => 5, + ], + self::PREVIOUSCOUNTER => [ + 'name' => 'previousCounter', + 'required' => false, + 'type' => 5, + ], + self::CIPHERTEXT => [ + 'name' => 'ciphertext', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::RATCHETKEY] = null; + $this->values[self::COUNTER] = null; + $this->values[self::PREVIOUSCOUNTER] = null; + $this->values[self::CIPHERTEXT] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'ratchetKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setRatchetKey($value) + { + return $this->set(self::RATCHETKEY, $value); + } + + /** + * Returns value of 'ratchetKey' property. + * + * @return string + */ + public function getRatchetKey() + { + return $this->get(self::RATCHETKEY); + } + + /** + * Sets value of 'counter' property. + * + * @param int $value Property value + * + * @return null + */ + public function setCounter($value) + { + return $this->set(self::COUNTER, $value); + } + + /** + * Returns value of 'counter' property. + * + * @return int + */ + public function getCounter() + { + return $this->get(self::COUNTER); + } + + /** + * Sets value of 'previousCounter' property. + * + * @param int $value Property value + * + * @return null + */ + public function setPreviousCounter($value) + { + return $this->set(self::PREVIOUSCOUNTER, $value); + } + + /** + * Returns value of 'previousCounter' property. + * + * @return int + */ + public function getPreviousCounter() + { + return $this->get(self::PREVIOUSCOUNTER); + } + + /** + * Sets value of 'ciphertext' property. + * + * @param string $value Property value + * + * @return null + */ + public function setCiphertext($value) + { + return $this->set(self::CIPHERTEXT, $value); + } + + /** + * Returns value of 'ciphertext' property. + * + * @return string + */ + public function getCiphertext() + { + return $this->get(self::CIPHERTEXT); + } +} + +/** + * PreKeyWhisperMessage message. + */ +class Textsecure_PreKeyWhisperMessage extends \ProtobufMessage +{ + /* Field index constants */ + const REGISTRATIONID = 5; + const PREKEYID = 1; + const SIGNEDPREKEYID = 6; + const BASEKEY = 2; + const IDENTITYKEY = 3; + const MESSAGE = 4; + + /* @var array Field descriptors */ + protected static $fields = [ + self::REGISTRATIONID => [ + 'name' => 'registrationId', + 'required' => false, + 'type' => 5, + ], + self::PREKEYID => [ + 'name' => 'preKeyId', + 'required' => false, + 'type' => 5, + ], + self::SIGNEDPREKEYID => [ + 'name' => 'signedPreKeyId', + 'required' => false, + 'type' => 5, + ], + self::BASEKEY => [ + 'name' => 'baseKey', + 'required' => false, + 'type' => 7, + ], + self::IDENTITYKEY => [ + 'name' => 'identityKey', + 'required' => false, + 'type' => 7, + ], + self::MESSAGE => [ + 'name' => 'message', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::REGISTRATIONID] = null; + $this->values[self::PREKEYID] = null; + $this->values[self::SIGNEDPREKEYID] = null; + $this->values[self::BASEKEY] = null; + $this->values[self::IDENTITYKEY] = null; + $this->values[self::MESSAGE] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'registrationId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setRegistrationId($value) + { + return $this->set(self::REGISTRATIONID, $value); + } + + /** + * Returns value of 'registrationId' property. + * + * @return int + */ + public function getRegistrationId() + { + return $this->get(self::REGISTRATIONID); + } + + /** + * Sets value of 'preKeyId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setPreKeyId($value) + { + return $this->set(self::PREKEYID, $value); + } + + /** + * Returns value of 'preKeyId' property. + * + * @return int + */ + public function getPreKeyId() + { + return $this->get(self::PREKEYID); + } + + /** + * Sets value of 'signedPreKeyId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setSignedPreKeyId($value) + { + return $this->set(self::SIGNEDPREKEYID, $value); + } + + /** + * Returns value of 'signedPreKeyId' property. + * + * @return int + */ + public function getSignedPreKeyId() + { + return $this->get(self::SIGNEDPREKEYID); + } + + /** + * Sets value of 'baseKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setBaseKey($value) + { + return $this->set(self::BASEKEY, $value); + } + + /** + * Returns value of 'baseKey' property. + * + * @return string + */ + public function getBaseKey() + { + return $this->get(self::BASEKEY); + } + + /** + * Sets value of 'identityKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setIdentityKey($value) + { + return $this->set(self::IDENTITYKEY, $value); + } + + /** + * Returns value of 'identityKey' property. + * + * @return string + */ + public function getIdentityKey() + { + return $this->get(self::IDENTITYKEY); + } + + /** + * Sets value of 'message' property. + * + * @param string $value Property value + * + * @return null + */ + public function setMessage($value) + { + return $this->set(self::MESSAGE, $value); + } + + /** + * Returns value of 'message' property. + * + * @return string + */ + public function getMessage() + { + return $this->get(self::MESSAGE); + } +} + +/** + * KeyExchangeMessage message. + */ +class Textsecure_KeyExchangeMessage extends \ProtobufMessage +{ + /* Field index constants */ + const ID = 1; + const BASEKEY = 2; + const RATCHETKEY = 3; + const IDENTITYKEY = 4; + const BASEKEYSIGNATURE = 5; + + /* @var array Field descriptors */ + protected static $fields = [ + self::ID => [ + 'name' => 'id', + 'required' => false, + 'type' => 5, + ], + self::BASEKEY => [ + 'name' => 'baseKey', + 'required' => false, + 'type' => 7, + ], + self::RATCHETKEY => [ + 'name' => 'ratchetKey', + 'required' => false, + 'type' => 7, + ], + self::IDENTITYKEY => [ + 'name' => 'identityKey', + 'required' => false, + 'type' => 7, + ], + self::BASEKEYSIGNATURE => [ + 'name' => 'baseKeySignature', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::ID] = null; + $this->values[self::BASEKEY] = null; + $this->values[self::RATCHETKEY] = null; + $this->values[self::IDENTITYKEY] = null; + $this->values[self::BASEKEYSIGNATURE] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'id' property. + * + * @param int $value Property value + * + * @return null + */ + public function setId($value) + { + return $this->set(self::ID, $value); + } + + /** + * Returns value of 'id' property. + * + * @return int + */ + public function getId() + { + return $this->get(self::ID); + } + + /** + * Sets value of 'baseKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setBaseKey($value) + { + return $this->set(self::BASEKEY, $value); + } + + /** + * Returns value of 'baseKey' property. + * + * @return string + */ + public function getBaseKey() + { + return $this->get(self::BASEKEY); + } + + /** + * Sets value of 'ratchetKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setRatchetKey($value) + { + return $this->set(self::RATCHETKEY, $value); + } + + /** + * Returns value of 'ratchetKey' property. + * + * @return string + */ + public function getRatchetKey() + { + return $this->get(self::RATCHETKEY); + } + + /** + * Sets value of 'identityKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setIdentityKey($value) + { + return $this->set(self::IDENTITYKEY, $value); + } + + /** + * Returns value of 'identityKey' property. + * + * @return string + */ + public function getIdentityKey() + { + return $this->get(self::IDENTITYKEY); + } + + /** + * Sets value of 'baseKeySignature' property. + * + * @param string $value Property value + * + * @return null + */ + public function setBaseKeySignature($value) + { + return $this->set(self::BASEKEYSIGNATURE, $value); + } + + /** + * Returns value of 'baseKeySignature' property. + * + * @return string + */ + public function getBaseKeySignature() + { + return $this->get(self::BASEKEYSIGNATURE); + } +} + +/** + * SenderKeyMessage message. + */ +class Textsecure_SenderKeyMessage extends \ProtobufMessage +{ + /* Field index constants */ + const ID = 1; + const ITERATION = 2; + const CIPHERTEXT = 3; + + /* @var array Field descriptors */ + protected static $fields = [ + self::ID => [ + 'name' => 'id', + 'required' => false, + 'type' => 5, + ], + self::ITERATION => [ + 'name' => 'iteration', + 'required' => false, + 'type' => 5, + ], + self::CIPHERTEXT => [ + 'name' => 'ciphertext', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::ID] = null; + $this->values[self::ITERATION] = null; + $this->values[self::CIPHERTEXT] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'id' property. + * + * @param int $value Property value + * + * @return null + */ + public function setId($value) + { + return $this->set(self::ID, $value); + } + + /** + * Returns value of 'id' property. + * + * @return int + */ + public function getId() + { + return $this->get(self::ID); + } + + /** + * Sets value of 'iteration' property. + * + * @param int $value Property value + * + * @return null + */ + public function setIteration($value) + { + return $this->set(self::ITERATION, $value); + } + + /** + * Returns value of 'iteration' property. + * + * @return int + */ + public function getIteration() + { + return $this->get(self::ITERATION); + } + + /** + * Sets value of 'ciphertext' property. + * + * @param string $value Property value + * + * @return null + */ + public function setCiphertext($value) + { + return $this->set(self::CIPHERTEXT, $value); + } + + /** + * Returns value of 'ciphertext' property. + * + * @return string + */ + public function getCiphertext() + { + return $this->get(self::CIPHERTEXT); + } +} + +/** + * SenderKeyDistributionMessage message. + */ +class Textsecure_SenderKeyDistributionMessage extends \ProtobufMessage +{ + /* Field index constants */ + const ID = 1; + const ITERATION = 2; + const CHAINKEY = 3; + const SIGNINGKEY = 4; + + /* @var array Field descriptors */ + protected static $fields = [ + self::ID => [ + 'name' => 'id', + 'required' => false, + 'type' => 5, + ], + self::ITERATION => [ + 'name' => 'iteration', + 'required' => false, + 'type' => 5, + ], + self::CHAINKEY => [ + 'name' => 'chainKey', + 'required' => false, + 'type' => 7, + ], + self::SIGNINGKEY => [ + 'name' => 'signingKey', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::ID] = null; + $this->values[self::ITERATION] = null; + $this->values[self::CHAINKEY] = null; + $this->values[self::SIGNINGKEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'id' property. + * + * @param int $value Property value + * + * @return null + */ + public function setId($value) + { + return $this->set(self::ID, $value); + } + + /** + * Returns value of 'id' property. + * + * @return int + */ + public function getId() + { + return $this->get(self::ID); + } + + /** + * Sets value of 'iteration' property. + * + * @param int $value Property value + * + * @return null + */ + public function setIteration($value) + { + return $this->set(self::ITERATION, $value); + } + + /** + * Returns value of 'iteration' property. + * + * @return int + */ + public function getIteration() + { + return $this->get(self::ITERATION); + } + + /** + * Sets value of 'chainKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setChainKey($value) + { + return $this->set(self::CHAINKEY, $value); + } + + /** + * Returns value of 'chainKey' property. + * + * @return string + */ + public function getChainKey() + { + return $this->get(self::CHAINKEY); + } + + /** + * Sets value of 'signingKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setSigningKey($value) + { + return $this->set(self::SIGNINGKEY, $value); + } + + /** + * Returns value of 'signingKey' property. + * + * @return string + */ + public function getSigningKey() + { + return $this->get(self::SIGNINGKEY); + } +} diff --git a/src/libaxolotl-php/ratchet/AliceAxolotlParameters.php b/src/libaxolotl-php/ratchet/AliceAxolotlParameters.php new file mode 100755 index 00000000..de725fbe --- /dev/null +++ b/src/libaxolotl-php/ratchet/AliceAxolotlParameters.php @@ -0,0 +1,131 @@ + + protected $theirRatchetKey; // ECPublicKey + + public function AliceAxolotlParameters($ourIdentityKey, $ourBaseKey, $theirIdentityKey, $theirSignedPreKey, $theirRatchetKey, $theirOneTimePreKey) // [IdentityKeyPair ourIdentityKey, ECKeyPair ourBaseKey, IdentityKey theirIdentityKey, ECPublicKey theirSignedPreKey, ECPublicKey theirRatchetKey, Optional theirOneTimePreKey] + { + $this->ourIdentityKey = $ourIdentityKey; + $this->ourBaseKey = $ourBaseKey; + $this->theirIdentityKey = $theirIdentityKey; + $this->theirSignedPreKey = $theirSignedPreKey; + $this->theirRatchetKey = $theirRatchetKey; + $this->theirOneTimePreKey = $theirOneTimePreKey; + if (($ourIdentityKey == null) || ($ourBaseKey == null) + || ($theirIdentityKey == null) || ($theirSignedPreKey == null) || ($theirRatchetKey == null)) { + throw new Exception('Null values!'); + } + } + + public function getOurIdentityKey() + { + return $this->ourIdentityKey; + } + + public function getOurBaseKey() + { + return $this->ourBaseKey; + } + + public function getTheirIdentityKey() + { + return $this->theirIdentityKey; + } + + public function getTheirSignedPreKey() + { + return $this->theirSignedPreKey; + } + + public function getTheirOneTimePreKey() + { + return $this->theirOneTimePreKey; + } + + public static function newBuilder() + { + return new AliceBuilder(); + } + + public function getTheirRatchetKey() + { + return $this->theirRatchetKey; + } +} +class AliceBuilder +{ + protected $ourIdentityKey; + protected $ourBaseKey; + protected $theirIdentityKey; + protected $theirSignedPreKey; + protected $theirRatchetKey; + protected $theirOneTimePreKey; + + public function AliceBuilder() + { + $this->ourIdentityKey = null; + $this->ourBaseKey = null; + $this->theirIdentityKey = null; + $this->theirSignedPreKey = null; + $this->theirRatchetKey = null; + $this->theirOneTimePreKey = null; + } + + public function setOurIdentityKey($ourIdentityKey) + { + $this->ourIdentityKey = $ourIdentityKey; + + return $this; + } + + public function setOurBaseKey($ourBaseKey) + { + $this->ourBaseKey = $ourBaseKey; + + return $this; + } + + public function setTheirRatchetKey($theirRatchetKey) + { + $this->theirRatchetKey = $theirRatchetKey; + + return $this; + } + + public function setTheirIdentityKey($theirIdentityKey) + { + $this->theirIdentityKey = $theirIdentityKey; + + return $this; + } + + public function setTheirSignedPreKey($theirSignedPreKey) + { + $this->theirSignedPreKey = $theirSignedPreKey; + + return $this; + } + + public function setTheirOneTimePreKey($theirOneTimePreKey) + { + $this->theirOneTimePreKey = $theirOneTimePreKey; + + return $this; + } + + public function create() + { + return new AliceAxolotlParameters($this->ourIdentityKey, $this->ourBaseKey, $this->theirIdentityKey, + $this->theirSignedPreKey, $this->theirRatchetKey, $this->theirOneTimePreKey); + } +} diff --git a/src/libaxolotl-php/ratchet/BobAxolotlParameters.php b/src/libaxolotl-php/ratchet/BobAxolotlParameters.php new file mode 100755 index 00000000..040f71bd --- /dev/null +++ b/src/libaxolotl-php/ratchet/BobAxolotlParameters.php @@ -0,0 +1,135 @@ + theirBaseKey] + { + $this->ourIdentityKey = $ourIdentityKey; + $this->ourSignedPreKey = $ourSignedPreKey; + $this->ourRatchetKey = $ourRatchetKey; + $this->ourOneTimePreKey = $ourOneTimePreKey; + $this->theirIdentityKey = $theirIdentityKey; + $this->theirBaseKey = $theirBaseKey; + if (($ourIdentityKey == null) || ($ourSignedPreKey == null) + || ($ourRatchetKey == null) + || ($theirIdentityKey == null) || ($theirBaseKey == null)) { + throw new Exception('Null values!'); + } + } + + public function getOurIdentityKey() + { + return $this->ourIdentityKey; + } + + public function getOurSignedPreKey() + { + return $this->ourSignedPreKey; + } + + public function getTheirIdentityKey() + { + return $this->theirIdentityKey; + } + + public function getOurRatchetKey() + { + return $this->ourRatchetKey; + } + + public function getTheirBaseKey() + { + return $this->theirBaseKey; + } + + public static function newBuilder() + { + return new BobBuilder(); + } + + public function getOurOneTimePreKey() + { + return $this->ourOneTimePreKey; + } +} +class BobBuilder +{ + protected $ourIdentityKey; + protected $ourSignedPreKey; + protected $ourRatchetKey; + protected $ourOneTimePreKey; + protected $theirIdentityKey; + protected $theirBaseKey; + + public function BobBuilder() + { + $this->ourIdentityKey = null; + $this->ourSignedPreKey = null; + $this->ourRatchetKey = null; + $this->ourOneTimePreKey = null; + $this->theirIdentityKey = null; + $this->theirBaseKey = null; + } + + public function setOurIdentityKey($ourIdentityKey) + { + $this->ourIdentityKey = $ourIdentityKey; + + return $this; + } + + public function setOurSignedPreKey($ourSignedPreKey) + { + $this->ourSignedPreKey = $ourSignedPreKey; + + return $this; + } + + public function setOurOneTimePreKey($ourOneTimePreKey) + { + $this->ourOneTimePreKey = $ourOneTimePreKey; + + return $this; + } + + public function setTheirIdentityKey($theirIdentityKey) + { + $this->theirIdentityKey = $theirIdentityKey; + + return $this; + } + + public function setOurRatchetKey($ourRatchetKey) + { + $this->ourRatchetKey = $ourRatchetKey; + + return $this; + } + + public function setTheirBaseKey($theirBaseKey) + { + $this->theirBaseKey = $theirBaseKey; + + return $this; + } + + public function create() + { + return new BobAxolotlParameters($this->ourIdentityKey, $this->ourSignedPreKey, $this->ourRatchetKey, $this->ourOneTimePreKey, + $this->theirIdentityKey, + $this->theirBaseKey); + } +} diff --git a/src/libaxolotl-php/ratchet/ChainKey.php b/src/libaxolotl-php/ratchet/ChainKey.php new file mode 100755 index 00000000..b84fc099 --- /dev/null +++ b/src/libaxolotl-php/ratchet/ChainKey.php @@ -0,0 +1,53 @@ +kdf = $kdf; + $this->key = $key; + $this->index = $index; + } + + public function getKey() + { + return $this->key; + } + + public function getIndex() + { + return $this->index; + } + + public function getNextChainKey() + { + $nextKey = $this->getBaseMaterial(self::CHAIN_KEY_SEED); + + return new self($this->kdf, $nextKey, $this->index + 1); + } + + public function getMessageKeys() + { + $inputKeyMaterial = $this->getBaseMaterial(self::MESSAGE_KEY_SEED); + $keyMaterialBytes = $this->kdf->deriveSecrets($inputKeyMaterial, 'WhisperMessageKeys', DerivedMessageSecrets::SIZE); + $keyMaterial = new DerivedMessageSecrets($keyMaterialBytes); + + return new MessageKeys($keyMaterial->getCipherKey(), $keyMaterial->getMacKey(), $keyMaterial->getIv(), $this->index); + } + + public function getBaseMaterial($seedBytes) + { + $mac = hash_init('sha256', HASH_HMAC, $this->key); + hash_update($mac, $seedBytes); + $data = hash_final($mac, true); + + return $data; + } + } diff --git a/src/libaxolotl-php/ratchet/MessageKeys.php b/src/libaxolotl-php/ratchet/MessageKeys.php new file mode 100755 index 00000000..9141c2d2 --- /dev/null +++ b/src/libaxolotl-php/ratchet/MessageKeys.php @@ -0,0 +1,37 @@ +cipherKey = $cipherKey; + $this->macKey = $macKey; + $this->iv = $iv; + $this->counter = $counter; + } + + public function getCipherKey() + { + return $this->cipherKey; + } + + public function getMacKey() + { + return $this->macKey; + } + + public function getIv() + { + return $this->iv; + } + + public function getCounter() + { + return $this->counter; + } +} diff --git a/src/libaxolotl-php/ratchet/RatchetingSession.php b/src/libaxolotl-php/ratchet/RatchetingSession.php new file mode 100755 index 00000000..52f664a8 --- /dev/null +++ b/src/libaxolotl-php/ratchet/RatchetingSession.php @@ -0,0 +1,164 @@ +getOurBaseKey()->getPublicKey(), $parameters->getTheirBaseKey())) { + $aliceParameters = AliceAxolotlParameters::newBuilder(); + $aliceParameters->setOurBaseKey($parameters->getOurBaseKey()) + ->setOurIdentityKey($parameters->getOurIdentityKey()) + ->setTheirRatchetKey($parameters->getTheirRatchetKey()) + ->setTheirIdentityKey($parameters->getTheirIdentityKey()) + ->setTheirSignedPreKey($parameters->getTheirBaseKey()) + ->setTheirOneTimePreKey(null); + self::initializeSessionAsAlice($sessionState, $sessionVersion, $aliceParameters->create()); + } else { + $bobParameters = BobAxolotlParameters::newBuilder(); + $bobParameters->setOurIdentityKey($parameters->getOurIdentityKey()) + ->setOurRatchetKey($parameters->getOurRatchetKey()) + ->setOurSignedPreKey($parameters->getOurBaseKey()) + ->setOurOneTimePreKey(null) + ->setTheirBaseKey($parameters->getTheirBaseKey()) + ->setTheirIdentityKey($parameters->getTheirIdentityKey()); + self::initializeSessionAsBob($sessionState, $sessionVersion, $bobParameters->create()); + } + } + + public static function initializeSessionAsAlice($sessionState, $sessionVersion, $parameters) + { + /* + :type sessionState: SessionState + :type sessionVersion: int + :type parameters: AliceAxolotlParameters + */ + $sessionState->setSessionVersion($sessionVersion); + $sessionState->setRemoteIdentityKey($parameters->getTheirIdentityKey()); + $sessionState->setLocalIdentityKey($parameters->getOurIdentityKey()->getPublicKey()); + + $sendingRatchetKey = Curve::generateKeyPair(); + $secrets = ''; + + if ($sessionVersion >= 3) { + $secrets .= self::getDiscontinuityBytes(); + } + + $secrets .= Curve::calculateAgreement($parameters->getTheirSignedPreKey(), + $parameters->getOurIdentityKey()->getPrivateKey()); + $secrets .= Curve::calculateAgreement($parameters->getTheirIdentityKey()->getPublicKey(), + $parameters->getOurBaseKey()->getPrivateKey()); + $secrets .= Curve::calculateAgreement($parameters->getTheirSignedPreKey(), + $parameters->getOurBaseKey()->getPrivateKey()); + + if ($sessionVersion >= 3 && $parameters->getTheirOneTimePreKey() != null) { + $secrets .= Curve::calculateAgreement($parameters->getTheirOneTimePreKey(), $parameters->getOurBaseKey()->getPrivateKey()); + } + + $derivedKeys = self::calculateDerivedKeys($sessionVersion, $secrets); + $sendingChain = $derivedKeys->getRootKey()->createChain($parameters->getTheirRatchetKey(), $sendingRatchetKey); + + $sessionState->addReceiverChain($parameters->getTheirRatchetKey(), $derivedKeys->getChainKey()); + $sessionState->setSenderChain($sendingRatchetKey, $sendingChain[1]); + $sessionState->setRootKey($sendingChain[0]); + } + + public static function initializeSessionAsBob($sessionState, $sessionVersion, $parameters) + { + /* + :type sessionState: SessionState + :type sessionVersion: int + :type parameters: BobAxolotlParameters + */ + + $sessionState->setSessionVersion($sessionVersion); + $sessionState->setRemoteIdentityKey($parameters->getTheirIdentityKey()); + $sessionState->setLocalIdentityKey($parameters->getOurIdentityKey()->getPublicKey()); + + $secrets = ''; + + if ($sessionVersion >= 3) { + $secrets .= self::getDiscontinuityBytes(); + } + + $secrets .= Curve::calculateAgreement($parameters->getTheirIdentityKey()->getPublicKey(), + $parameters->getOurSignedPreKey()->getPrivateKey()); + $secrets .= Curve::calculateAgreement($parameters->getTheirBaseKey(), + $parameters->getOurIdentityKey()->getPrivateKey()); + + $secrets .= Curve::calculateAgreement($parameters->getTheirBaseKey(), + $parameters->getOurSignedPreKey()->getPrivateKey()); + + if ($sessionVersion >= 3 && $parameters->getOurOneTimePreKey() != null) { + $secrets .= Curve::calculateAgreement($parameters->getTheirBaseKey(), + $parameters->getOurOneTimePreKey()->getPrivateKey()); + } + + $derivedKeys = self::calculateDerivedKeys($sessionVersion, $secrets); + $sessionState->setSenderChain($parameters->getOurRatchetKey(), $derivedKeys->getChainKey()); + $sessionState->setRootKey($derivedKeys->getRootKey()); + } + + public static function getDiscontinuityBytes() + { + return str_repeat("\xFF", 32); + } + + public static function calculateDerivedKeys($sessionVersion, $masterSecret) + { + $kdf = HKDF::createFor($sessionVersion); + $derivedSecretBytes = $kdf->deriveSecrets($masterSecret, 'WhisperText', 64); + $derivedSecrets = ByteUtil::split($derivedSecretBytes, 32, 32); + + return new DerivedKeys(new RootKey($kdf, $derivedSecrets[0]), + new ChainKey($kdf, $derivedSecrets[1], 0)); + } + + public static function isAlice($ourKey, $theirKey) + { + /* + :type ourKey: ECPublicKey + :type theirKey: ECPublicKey + */ + return $ourKey->compareTo($theirKey) == -1; + } +} +class DerivedKeys +{ + protected $rootKey; + protected $chainKey; + + public function DerivedKeys($rootKey, $chainKey) + { + /* + :type rootKey: RootKey + :type chainKey: ChainKey + */ + $this->rootKey = $rootKey; + $this->chainKey = $chainKey; + } + + public function getRootKey() + { + return $this->rootKey; + } + + public function getChainKey() + { + return $this->chainKey; + } +} diff --git a/src/libaxolotl-php/ratchet/RootKey.php b/src/libaxolotl-php/ratchet/RootKey.php new file mode 100755 index 00000000..03dbcdce --- /dev/null +++ b/src/libaxolotl-php/ratchet/RootKey.php @@ -0,0 +1,31 @@ +kdf = $kdf; + $this->key = $key; + } + + public function getKeyBytes() + { + return $this->key; + } + + public function createChain($ECPublicKey_theirRatchetKey, $ECKeyPair_ourRatchetKey) + { + $sharedSecret = Curve::calculateAgreement($ECPublicKey_theirRatchetKey, $ECKeyPair_ourRatchetKey->getPrivateKey()); + + $derivedSecretBytes = $this->kdf->deriveSecrets($sharedSecret, 'WhisperRatchet', DerivedRootSecrets::SIZE, $this->key); + $derivedSecrets = new DerivedRootSecrets($derivedSecretBytes); + $newRootKey = new self($this->kdf, $derivedSecrets->getRootKey()); + $newChainKey = new ChainKey($this->kdf, $derivedSecrets->getChainKey(), 0); + + return [$newRootKey, $newChainKey]; + } +} diff --git a/src/libaxolotl-php/ratchet/SymmetricAxolotlParameters.php b/src/libaxolotl-php/ratchet/SymmetricAxolotlParameters.php new file mode 100755 index 00000000..36ee6ccc --- /dev/null +++ b/src/libaxolotl-php/ratchet/SymmetricAxolotlParameters.php @@ -0,0 +1,133 @@ +ourBaseKey = $ourBaseKey; + $this->ourRatchetKey = $ourRatchetKey; + $this->ourIdentityKey = $ourIdentityKey; + $this->theirBaseKey = $theirBaseKey; + $this->theirRatchetKey = $theirRatchetKey; + $this->theirIdentityKey = $theirIdentityKey; + + if (($ourBaseKey == null) || ($ourRatchetKey == null) + || ($ourIdentityKey == null) || ($theirBaseKey == null) + || ($theirRatchetKey == null) || ($theirIdentityKey == null)) { + throw new Exception('Null values!'); + } + } + + public function getOurBaseKey() + { + return $this->ourBaseKey; + } + + public function getOurRatchetKey() + { + return $this->ourRatchetKey; + } + + public function getOurIdentityKey() + { + return $this->ourIdentityKey; + } + + public function getTheirBaseKey() + { + return $this->theirBaseKey; + } + + public function getTheirRatchetKey() + { + return $this->theirRatchetKey; + } + + public function getTheirIdentityKey() + { + return $this->theirIdentityKey; + } + + public static function newBuilder() + { + return new SymmetricBuilder(); + } +} +class SymmetricBuilder +{ + protected $ourBaseKey; // ECKeyPair + protected $ourRatchetKey; // ECKeyPair + protected $ourIdentityKey; // IdentityKeyPair + protected $theirBaseKey; // ECPublicKey + protected $theirRatchetKey; // ECPublicKey + protected $theirIdentityKey; // IdentityKey + + public function SymmetricBuilder() + { + $this->ourIdentityKey = null; + $this->ourBaseKey = null; + $this->ourRatchetKey = null; + $this->theirRatchetKey = null; + $this->theirIdentityKey = null; + $this->theirBaseKey = null; + } + + public function setOurIdentityKey($ourIdentityKey) + { + $this->ourIdentityKey = $ourIdentityKey; + + return $this; + } + + public function setOurBaseKey($ourBaseKey) + { + $this->ourBaseKey = $ourBaseKey; + + return $this; + } + + public function setOurRatchetKey($ourRatchetKey) + { + $this->ourRatchetKey = $ourRatchetKey; + + return $this; + } + + public function setTheirRatchetKey($theirRatchetKey) + { + $this->theirRatchetKey = $theirRatchetKey; + + return $this; + } + + public function setTheirIdentityKey($theirIdentityKey) + { + $this->theirIdentityKey = $theirIdentityKey; + + return $this; + } + + public function setTheirBaseKey($theirBaseKey) + { + $this->theirBaseKey = $theirBaseKey; + + return $this; + } + + public function create() + { + return new SymmetricAxolotlParameters($this->ourBaseKey, $this->ourRatchetKey, $this->ourIdentityKey, + $this->theirBaseKey, $this->theirRatchetKey, $this->theirIdentityKey); + } +} diff --git a/src/libaxolotl-php/state/AxolotlStore.php b/src/libaxolotl-php/state/AxolotlStore.php new file mode 100755 index 00000000..d3a24544 --- /dev/null +++ b/src/libaxolotl-php/state/AxolotlStore.php @@ -0,0 +1,6 @@ +registrationId = $registrationId; + $this->deviceId = $deviceId; + $this->preKeyId = $preKeyId; + $this->preKeyPublic = $preKeyPublic; + $this->signedPreKeyId = $signedPreKeyId; + $this->signedPreKeyPublic = $signedPreKeyPublic; + $this->signedPreKeySignature = $signedPreKeySignature; + $this->identityKey = $identityKey; + } + + public function getDeviceId() + { + return $this->deviceId; + } + + public function getPreKeyId() + { + return $this->preKeyId; + } + + public function getPreKey() + { + return $this->preKeyPublic; + } + + public function getSignedPreKeyId() + { + return $this->signedPreKeyId; + } + + public function getSignedPreKey() + { + return $this->signedPreKeyPublic; + } + + public function getSignedPreKeySignature() + { + return $this->signedPreKeySignature; + } + + public function getIdentityKey() + { + return $this->identityKey; + } + + public function getRegistrationId() + { + return $this->registrationId; + } +} diff --git a/src/libaxolotl-php/state/PreKeyRecord.php b/src/libaxolotl-php/state/PreKeyRecord.php new file mode 100755 index 00000000..b02866e3 --- /dev/null +++ b/src/libaxolotl-php/state/PreKeyRecord.php @@ -0,0 +1,44 @@ +structure = new Textsecure_PreKeyRecordStructure(); + if ($serialized == null) { + $this->structure->setId($id)->setPublicKey((string) $keyPair->getPublicKey()->serialize())->setPrivateKey((string) $keyPair->getPrivateKey()->serialize()); + } else { + try { + $this->structure->parseFromString($serialized); + } catch (Exception $ex) { + throw new Exception('Cannot unserialize PreKEyRecordStructure'); + } + } + } + + public function getId() + { + return $this->structure->getId(); + } + + public function getKeyPair() + { + $publicKey = Curve::decodePoint($this->structure->getPublicKey(), 0); + $privateKey = Curve::decodePrivatePoint($this->structure->getPrivateKey()); + + return new ECKeyPair($publicKey, $privateKey); + } + + public function serialize() + { + return $this->structure->serializeToString(); + } +} diff --git a/src/libaxolotl-php/state/PreKeyStore.php b/src/libaxolotl-php/state/PreKeyStore.php new file mode 100755 index 00000000..fb51f62e --- /dev/null +++ b/src/libaxolotl-php/state/PreKeyStore.php @@ -0,0 +1,21 @@ +previousStates = []; + if ($sessionState != null) { + $this->sessionState = $sessionState; + $this->fresh = false; + } elseif ($serialized != null) { + $record = new Textsecure_RecordStructure(); + $record->parseFromString($serialized); + $this->sessionState = new SessionState($record->getCurrentSession()); + $this->fresh = false; + foreach ($record->getPreviousSessions() as $previousStructure) { + $this->previousStates[] = new SessionState($previousStructure); + } + } else { + $this->fresh = true; + $this->sessionState = new SessionState(); + } + } + + public function hasSessionState($version, $aliceBaseKey) + { + if ($this->sessionState->getSessionVersion() == $version && $aliceBaseKey == $this->sessionState->getAliceBaseKey()) { + return true; + } + + foreach ($this->previousStates as $state) { + if ($state->getSessionVersion() == $version && $aliceBaseKey == $state->getAliceBaseKey()) { + return true; + } + } + + return false; + } + + public function getSessionState() + { + return $this->sessionState; + } + + public function getPreviousSessionStates() + { + return $this->previousStates; + } + + public function removePreviousSessionStateAt($i) + { + if (isset($this->previousStates[$i])) { + unset($this->previousStates[$i]); + $this->previousStates = array_values($this->previousStates); + } + } + + public function isFresh() + { + return $this->fresh; + } + + public function archiveCurrentState() + { + $this->promoteState(new SessionState()); + } + + public function promoteState($promotedState) + { + array_unshift($this->previousStates, $this->sessionState); + $this->sessionState = $promotedState; + if (count($this->previousStates) > self::ARCHIVED_STATES_MAX_LENGTH) { + array_pop($this->previousStates); + } + } + + public function setState($sessionState) + { + $this->sessionState = $sessionState; + } + + public function serialize() + { + $previousStructures = []; + //previousState.getStructure() for previousState in self.previousStates + $record = new Textsecure_RecordStructure(); + $record->setCurrentSession($this->sessionState->getStructure()); + foreach ($this->previousStates as $previousState) { + $record->appendPreviousSessions($previousState->getStructure()); + } + /* + Python + record.currentSession.MergeFrom(self.sessionState.getStructure()) + record.previousSessions.extend(previousStructures) + + return record.SerializeToString() + */ + return $record->serializeToString(); + } +} diff --git a/src/libaxolotl-php/state/SessionState.php b/src/libaxolotl-php/state/SessionState.php new file mode 100755 index 00000000..11599956 --- /dev/null +++ b/src/libaxolotl-php/state/SessionState.php @@ -0,0 +1,421 @@ +sessionStructure = new Textsecure_SessionStructure(); + } elseif ($session instanceof self) { + $this->sessionStructure = new Textsecure_SessionStructure(); + $this->sessionStructure->parseFromString($session->getStructure()->serializeToString()); + } else { + $this->sessionStructure = $session; + } + } + + public function getStructure() + { + return $this->sessionStructure; + } + + public function getAliceBaseKey() + { + return $this->sessionStructure->getAliceBaseKey(); + } + + public function setAliceBaseKey($aliceBaseKey) + { + $this->sessionStructure->setAliceBaseKey($aliceBaseKey); + } + + public function setSessionVersion($version) + { + $this->sessionStructure->setSessionVersion($version); + } + + public function getSessionVersion() + { + $sessionVersion = $this->sessionStructure->getSessionVersion(); + + return $sessionVersion == 0 ? 2 : $sessionVersion; + } + + public function setRemoteIdentityKey($identityKey) + { + $this->sessionStructure->setRemoteIdentityPublic($identityKey->serialize()); + } + + public function setLocalIdentityKey($identityKey) + { + $this->sessionStructure->setLocalIdentityPublic($identityKey->serialize()); + } + + public function getRemoteIdentityKey() + { + if ($this->sessionStructure->getRemoteIdentityPublic() == null) { + return; + } + + return new IdentityKey($this->sessionStructure->getRemoteIdentityPublic(), 0); + } + + public function getLocalIdentityKey() + { + return new IdentityKey($this->sessionStructure->getLocalIdentityPublic(), 0); + } + + public function getPreviousCounter() + { + return $this->sessionStructure->getPreviousCounter(); + } + + public function setPreviousCounter($previousCounter) + { + $this->sessionStructure->setPreviousCounter($previousCounter); + } + + public function getRootKey() + { + return new RootKey(HKDF::createFor($this->getSessionVersion()), $this->sessionStructure->getRootKey()); + } + + public function setRootKey($rootKey) + { + $this->sessionStructure->setRootKey($rootKey->getKeyBytes()); + } + + public function getSenderRatchetKey() + { + return Curve::decodePoint($this->sessionStructure->getSenderChain()->getSenderRatchetKey(), 0); + } + + public function getSenderRatchetKeyPair() + { + $publicKey = $this->getSenderRatchetKey(); + $privateKey = Curve::decodePrivatePoint($this->sessionStructure->getSenderChain()->getSenderRatchetKeyPrivate()); + + return new ECKeyPair($publicKey, $privateKey); + } + + public function hasReceiverChain($ECPublickKey_senderEphemeral) + { + return $this->getReceiverChain($ECPublickKey_senderEphemeral) != null; + } + + public function hasSenderChain() + { + return $this->sessionStructure->getSenderChain() != null; + } + + public function getReceiverChain($ECPublickKey_senderEphemeral) + { + $receiverChains = $this->sessionStructure->getReceiverChains(); + $index = 0; + foreach ($receiverChains as $receiverChain) { + $chainSenderRatchetKey = Curve::decodePoint($receiverChain->getSenderRatchetKey(), 0); + if ($chainSenderRatchetKey == $ECPublickKey_senderEphemeral) { + return [$receiverChain, $index]; + } + $index += 1; + } + } + + public function getReceiverChainKey($ECPublicKey_senderEphemeral) + { + $receiverChainAndIndex = $this->getReceiverChain($ECPublicKey_senderEphemeral); + $receiverChain = $receiverChainAndIndex[0]; + if ($receiverChain == null) { + return; + } + + return new ChainKey(HKDF::createFor($this->getSessionVersion()), + $receiverChain->getChainKey()->getKey(), + $receiverChain->getChainKey()->getIndex()); + } + + public function addReceiverChain($ECPublickKey_senderRatchetKey, $chainKey) + { + $senderRatchetKey = $ECPublickKey_senderRatchetKey; + + $chain = new Textsecure_SessionStructure_Chain(); + $chain->setSenderRatchetKey($senderRatchetKey->serialize()); + $chain->setChainKey(new Textsecure_SessionStructure_Chain_ChainKey()); + $chain->getChainKey()->setKey($chainKey->getKey()); + $chain->getChainKey()->setIndex($chainKey->getIndex()); + + $this->sessionStructure->appendReceiverChains($chain); + + if (count($this->sessionStructure->getReceiverChains()) > 5) { + $chains = $this->sessionStructure->getReceiverChains(); + $chains = array_slice($chains, 1); + $this->sessionStructure->clearReceiverChains(); + foreach ($chains as $chain) { + $this->sessionStructure->appendReceiverChains($chain); + } + //$this->sessionStructure->setReceiverChains($chains); + } + } + + public function setSenderChain($ECKeyPair_senderRatchetKeyPair, $chainKey) + { + $senderRatchetKeyPair = $ECKeyPair_senderRatchetKeyPair; + + $senderChain = new Textsecure_SessionStructure_Chain(); + $this->sessionStructure->setSenderChain($senderChain); + $this->sessionStructure->getSenderChain()->setSenderRatchetKey($senderRatchetKeyPair->getPublicKey()->serialize()); + $this->sessionStructure->getSenderChain()->setSenderRatchetKeyPrivate($senderRatchetKeyPair->getPrivateKey()->serialize()); + $this->sessionStructure->getSenderChain()->setChainKey(new Textsecure_SessionStructure_Chain_ChainKey()); + $this->sessionStructure->getSenderChain()->getChainKey()->setKey($chainKey->getKey()); + $this->sessionStructure->getSenderChain()->getChainKey()->setIndex($chainKey->getIndex()); + } + + public function getSenderChainKey() + { + $chainKeyStructure = $this->sessionStructure->getSenderChain()->getChainKey(); + + return new ChainKey(HKDF::createFor($this->getSessionVersion()), + $chainKeyStructure->getKey(), $chainKeyStructure->getIndex()); + } + + public function setSenderChainKey($ChainKey_nextChainKey) + { + $nextChainKey = $ChainKey_nextChainKey; + + $this->sessionStructure->getSenderChain()->getChainKey()->setKey($nextChainKey->getKey()); + $this->sessionStructure->getSenderChain()->getChainKey()->setIndex($nextChainKey->getIndex()); + } + + public function hasMessageKeys($ECPublickKey_senderEphemeral, $counter) + { + $senderEphemeral = $ECPublickKey_senderEphemeral; + $chainAndIndex = $this->getReceiverChain($senderEphemeral); + $chain = $chainAndIndex[0]; + if ($chain == null) { + return false; + } + + $messageKeyList = $chain->getMessageKeys(); + foreach ($messageKeyList as $messageKey) { + if ($messageKey->getIndex() == $counter) { + return true; + } + } + + return false; + } + + public function removeMessageKeys($ECPublicKey_senderEphemeral, $counter) + { + $senderEphemeral = $ECPublicKey_senderEphemeral; + $chainAndIndex = $this->getReceiverChain($senderEphemeral); + $chain = $chainAndIndex[0]; + if ($chain == null) { + return; + } + + $messageKeyList = $chain->getMessageKeys(); + $result = null; + + for ($i = 0; $i < count($messageKeyList); $i++) { + $messageKey = $messageKeyList[$i]; + if ($messageKey->getIndex() == $counter) { + $result = new MessageKeys($messageKey->getCipherKey(), $messageKey->getMacKey(), $messageKey->getIv(), $messageKey->getIndex()); + unset($messageKeyList[$i]); + //del messageKeyList[i] <- 1. is a copy of the original array so go to 2 + break; + } + } + $chain->clearMessageKeys(); + foreach ($messageKeyList as $msgKey) { + $chain->appendMessageKeys($msgKey); + } + $this->sessionStructure->getReceiverChains()[$chainAndIndex[1]]->parseFromString($chain->serializeToString()); + + return $result; + } + + public function setMessageKeys($ECPublicKey_senderEphemeral, $messageKeys) + { + $senderEphemeral = $ECPublicKey_senderEphemeral; + $chainAndIndex = $this->getReceiverChain($senderEphemeral); + $chain = $chainAndIndex[0]; + $messageKeyStructure = new Textsecure_SessionStructure_Chain_MessageKey(); //$chain->messageKeys.add() #storageprotos.SessionStructure.Chain.MessageKey() + $messageKeyStructure->setCipherKey($messageKeys->getCipherKey()); + $messageKeyStructure->setMacKey($messageKeys->getMacKey()); + $messageKeyStructure->setIndex($messageKeys->getCounter()); + $messageKeyStructure->setIv($messageKeys->getIv()); + $chain->appendMessageKeys($messageKeyStructure); //$chain->messageKeys.add() + + //chain.messageKeys.append(messageKeyStructure) + + $this->sessionStructure->getReceiverChains()[$chainAndIndex[1]]->parseFromString($chain->serializeToString()); + } + + public function setReceiverChainKey($ECPublicKey_senderEphemeral, $chainKey) + { + $senderEphemeral = $ECPublicKey_senderEphemeral; + $chainAndIndex = $this->getReceiverChain($senderEphemeral); + $chain = $chainAndIndex[0]; + $chain->getChainKey()->setKey($chainKey->getKey()); + $chain->getChainKey()->setIndex($chainKey->getIndex()); + + //$this->sessionStructure.receiverChains[chainAndIndex[1]].ClearField() + $this->sessionStructure->getReceiverChains()[$chainAndIndex[1]]->parseFromString($chain->serializeToString()); + } + + public function setPendingKeyExchange($sequence, $ourBaseKey, $ourRatchetKey, $ourIdentityKey) + { + /* + :type sequence: int + :type ourBaseKey: ECKeyPair + :type ourRatchetKey: ECKeyPair + :type ourIdentityKey: IdentityKeyPair + */ + + $structure = $this->sessionStructure->getPendingKeyExchange(); + if ($structure == null) { + $structure = new Textsecure_SessionStructure_PendingKeyExchange(); + } + $structure->setSequence($sequence); + $structure->setLocalBaseKey($ourBaseKey->getPublicKey()->serialize()); + $structure->setLocalBaseKeyPrivate($ourBaseKey->getPrivateKey()->serialize()); + $structure->setLocalRatchetKey($ourRatchetKey->getPublicKey()->serialize()); + $structure->setLocalRatchetKeyPrivate($ourRatchetKey->getPrivateKey()->serialize()); + $structure->setLocalIdentityKey($ourIdentityKey->getPublicKey()->serialize()); + $structure->setLocalIdentityKeyPrivate($ourIdentityKey->getPrivateKey()->serialize()); + + $this->sessionStructure->setPendingKeyExchange($structure); // should be the same as merge since it only have string/int fields and none of them repeated + } + + public function getPendingKeyExchangeSequence() + { + return $this->sessionStructure->getPendingKeyExchange()->getSequence(); + } + + public function getPendingKeyExchangeBaseKey() + { + $publicKey = Curve::decodePoint($this->sessionStructure->getPendingKeyExchange()->getLocalBaseKey(), 0); + $privateKey = Curve::decodePrivatePoint($this->sessionStructure->getPendingKeyExchange()->getLocalBaseKeyPrivate()); + + return new ECKeyPair($publicKey, $privateKey); + } + + public function getPendingKeyExchangeRatchetKey() + { + $publicKey = Curve::decodePoint($this->sessionStructure->getPendingKeyExchange()->getLocalRatchetKey(), 0); + $privateKey = Curve::decodePrivatePoint($this->sessionStructure->getPendingKeyExchange()->getLocalRatchetKeyPrivate()); + + return new ECKeyPair($publicKey, $privateKey); + } + + public function getPendingKeyExchangeIdentityKey() + { + $publicKey = new IdentityKey($this->sessionStructure->getPendingKeyExchange()->getLocalIdentityKey(), 0); + + $privateKey = Curve::decodePrivatePoint($this->sessionStructure->getPendingKeyExchange()->getLocalIdentityKeyPrivate()); + + return new IdentityKeyPair($publicKey, $privateKey); + } + + public function hasPendingKeyExchange() + { + return $this->sessionStructure->getPendingKeyExchange() != null; + } + + public function setUnacknowledgedPreKeyMessage($preKeyId, $signedPreKeyId, $baseKey) + { + /* + :type preKeyId: int + :type signedPreKeyId: int + :type baseKey: ECPublicKey + */ + if (!$this->hasUnacknowledgedPreKeyMessage()) { + $this->sessionStructure->setPendingPreKey(new Textsecure_SessionStructure_PendingPreKey()); + } + $this->sessionStructure->getPendingPreKey()->setSignedPreKeyId($signedPreKeyId); + $this->sessionStructure->getPendingPreKey()->setBaseKey($baseKey->serialize()); + + if ($preKeyId != null) { + $this->sessionStructure->getPendingPreKey()->setPreKeyId($preKeyId); + } + } + + public function hasUnacknowledgedPreKeyMessage() + { + return $this->sessionStructure->getPendingPreKey() != null; + } + + public function getUnacknowledgedPreKeyMessageItems() + { + $preKeyId = null; + if ($this->sessionStructure->getPendingPreKey()->getPreKeyId() != null) { + $preKeyId = $this->sessionStructure->getPendingPreKey()->getPreKeyId(); + } + + return new UnacknowledgedPreKeyMessageItems($preKeyId, + $this->sessionStructure->getPendingPreKey()->getSignedPreKeyId(), + Curve::decodePoint($this->sessionStructure->getPendingPreKey()->getBaseKey(), 0)); + } + + public function clearUnacknowledgedPreKeyMessage() + { + $this->sessionStructure->clearPendingPreKey(); + } + + public function setRemoteRegistrationId($registrationId) + { + $this->sessionStructure->setRemoteRegistrationId($registrationId); + } + + public function getRemoteRegistrationId($registrationId) + { + return $this->sessionStructure->getRemoteRegistrationId(); + } + + public function setLocalRegistrationId($registrationId) + { + $this->sessionStructure->setLocalRegistrationId($registrationId); + } + + public function getLocalRegistrationId() + { + return $this->sessionStructure->getLocalRegistrationId(); + } + + public function serialize() + { + return $this->sessionStructure->serializeToString(); + } +} +class UnacknowledgedPreKeyMessageItems +{ + public function UnacknowledgedPreKeyMessageItems($preKeyId, $signedPreKeyId, $baseKey) + { + /* + :type preKeyId: int + :type signedPreKeyId: int + :type baseKey: ECPublicKey + */ + $this->preKeyId = $preKeyId; + $this->signedPreKeyId = $signedPreKeyId; + $this->baseKey = $baseKey; + } + + public function getPreKeyId() + { + return $this->preKeyId; + } + + public function getSignedPreKeyId() + { + return $this->signedPreKeyId; + } + + public function getBaseKey() + { + return $this->baseKey; + } +} diff --git a/src/libaxolotl-php/state/SessionStore.php b/src/libaxolotl-php/state/SessionStore.php new file mode 100755 index 00000000..9781a8ac --- /dev/null +++ b/src/libaxolotl-php/state/SessionStore.php @@ -0,0 +1,28 @@ +setId($id); + $struct->setPublicKey((string) $keyPair->getPublicKey()->serialize()); + $struct->setPrivateKey((string) $keyPair->getPrivateKey()->serialize()); + $struct->setSignature((string) $signature); + $struct->setTimestamp($timestamp); + } else { + $struct->parseFromString($serialized); + } + $this->structure = $struct; //$SignedPreKeyRecordStructure->newBuilder()->setId($id)->setPublicKey($ByteString->copyFrom($keyPair->getPublicKey()->serialize()))->setPrivateKey($ByteString->copyFrom($keyPair->getPrivateKey()->serialize()))->setSignature($ByteString->copyFrom($signature))->setTimestamp($timestamp)->build(); + } + + public function getId() + { + return $this->structure->getId(); + } + + public function getTimestamp() + { + return $this->structure->getTimestamp(); + } + + public function getKeyPair() + { + try { + $publicKey = Curve::decodePoint($this->structure->getPublicKey(), 0); + $privateKey = Curve::decodePrivatePoint($this->structure->getPrivateKey()); + + return new ECKeyPair($publicKey, $privateKey); + } catch (InvalidKeyException $e) { + throw new Exception($e->getMessage()); + } + } + + public function getSignature() + { + return $this->structure->getSignature(); + } + + public function serialize() + { + return $this->structure->serializeToString(); + } +} diff --git a/src/libaxolotl-php/state/SignedPreKeyStore.php b/src/libaxolotl-php/state/SignedPreKeyStore.php new file mode 100755 index 00000000..14953889 --- /dev/null +++ b/src/libaxolotl-php/state/SignedPreKeyStore.php @@ -0,0 +1,23 @@ + [ + 'name' => 'index', + 'required' => false, + 'type' => 5, + ], + self::KEY => [ + 'name' => 'key', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::INDEX] = null; + $this->values[self::KEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'index' property. + * + * @param int $value Property value + * + * @return null + */ + public function setIndex($value) + { + return $this->set(self::INDEX, $value); + } + + /** + * Returns value of 'index' property. + * + * @return int + */ + public function getIndex() + { + return $this->get(self::INDEX); + } + + /** + * Sets value of 'key' property. + * + * @param string $value Property value + * + * @return null + */ + public function setKey($value) + { + return $this->set(self::KEY, $value); + } + + /** + * Returns value of 'key' property. + * + * @return string + */ + public function getKey() + { + return $this->get(self::KEY); + } +} + +/** + * MessageKey message embedded in Chain/SessionStructure message. + */ +class Textsecure_SessionStructure_Chain_MessageKey extends \ProtobufMessage +{ + /* Field index constants */ + const INDEX = 1; + const CIPHERKEY = 2; + const MACKEY = 3; + const IV = 4; + + /* @var array Field descriptors */ + protected static $fields = [ + self::INDEX => [ + 'name' => 'index', + 'required' => false, + 'type' => 5, + ], + self::CIPHERKEY => [ + 'name' => 'cipherKey', + 'required' => false, + 'type' => 7, + ], + self::MACKEY => [ + 'name' => 'macKey', + 'required' => false, + 'type' => 7, + ], + self::IV => [ + 'name' => 'iv', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::INDEX] = null; + $this->values[self::CIPHERKEY] = null; + $this->values[self::MACKEY] = null; + $this->values[self::IV] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'index' property. + * + * @param int $value Property value + * + * @return null + */ + public function setIndex($value) + { + return $this->set(self::INDEX, $value); + } + + /** + * Returns value of 'index' property. + * + * @return int + */ + public function getIndex() + { + return $this->get(self::INDEX); + } + + /** + * Sets value of 'cipherKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setCipherKey($value) + { + return $this->set(self::CIPHERKEY, $value); + } + + /** + * Returns value of 'cipherKey' property. + * + * @return string + */ + public function getCipherKey() + { + return $this->get(self::CIPHERKEY); + } + + /** + * Sets value of 'macKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setMacKey($value) + { + return $this->set(self::MACKEY, $value); + } + + /** + * Returns value of 'macKey' property. + * + * @return string + */ + public function getMacKey() + { + return $this->get(self::MACKEY); + } + + /** + * Sets value of 'iv' property. + * + * @param string $value Property value + * + * @return null + */ + public function setIv($value) + { + return $this->set(self::IV, $value); + } + + /** + * Returns value of 'iv' property. + * + * @return string + */ + public function getIv() + { + return $this->get(self::IV); + } +} + +/** + * Chain message embedded in SessionStructure message. + */ +class Textsecure_SessionStructure_Chain extends \ProtobufMessage +{ + /* Field index constants */ + const SENDERRATCHETKEY = 1; + const SENDERRATCHETKEYPRIVATE = 2; + const CHAINKEY = 3; + const MESSAGEKEYS = 4; + + /* @var array Field descriptors */ + protected static $fields = [ + self::SENDERRATCHETKEY => [ + 'name' => 'senderRatchetKey', + 'required' => false, + 'type' => 7, + ], + self::SENDERRATCHETKEYPRIVATE => [ + 'name' => 'senderRatchetKeyPrivate', + 'required' => false, + 'type' => 7, + ], + self::CHAINKEY => [ + 'name' => 'chainKey', + 'required' => false, + 'type' => 'Textsecure_SessionStructure_Chain_ChainKey', + ], + self::MESSAGEKEYS => [ + 'name' => 'messageKeys', + 'repeated' => true, + 'type' => 'Textsecure_SessionStructure_Chain_MessageKey', + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::SENDERRATCHETKEY] = null; + $this->values[self::SENDERRATCHETKEYPRIVATE] = null; + $this->values[self::CHAINKEY] = null; + $this->values[self::MESSAGEKEYS] = []; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'senderRatchetKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setSenderRatchetKey($value) + { + return $this->set(self::SENDERRATCHETKEY, $value); + } + + /** + * Returns value of 'senderRatchetKey' property. + * + * @return string + */ + public function getSenderRatchetKey() + { + return $this->get(self::SENDERRATCHETKEY); + } + + /** + * Sets value of 'senderRatchetKeyPrivate' property. + * + * @param string $value Property value + * + * @return null + */ + public function setSenderRatchetKeyPrivate($value) + { + return $this->set(self::SENDERRATCHETKEYPRIVATE, $value); + } + + /** + * Returns value of 'senderRatchetKeyPrivate' property. + * + * @return string + */ + public function getSenderRatchetKeyPrivate() + { + return $this->get(self::SENDERRATCHETKEYPRIVATE); + } + + /** + * Sets value of 'chainKey' property. + * + * @param Textsecure_SessionStructure_Chain_ChainKey $value Property value + * + * @return null + */ + public function setChainKey(Textsecure_SessionStructure_Chain_ChainKey $value) + { + return $this->set(self::CHAINKEY, $value); + } + + /** + * Returns value of 'chainKey' property. + * + * @return Textsecure_SessionStructure_Chain_ChainKey + */ + public function getChainKey() + { + return $this->get(self::CHAINKEY); + } + + /** + * Appends value to 'messageKeys' list. + * + * @param Textsecure_SessionStructure_Chain_MessageKey $value Value to append + * + * @return null + */ + public function appendMessageKeys(Textsecure_SessionStructure_Chain_MessageKey $value) + { + return $this->append(self::MESSAGEKEYS, $value); + } + + /** + * Clears 'messageKeys' list. + * + * @return null + */ + public function clearMessageKeys() + { + return $this->clear(self::MESSAGEKEYS); + } + + /** + * Returns 'messageKeys' list. + * + * @return Textsecure_SessionStructure_Chain_MessageKey[] + */ + public function getMessageKeys() + { + return $this->get(self::MESSAGEKEYS); + } + + /** + * Returns 'messageKeys' iterator. + * + * @return ArrayIterator + */ + public function getMessageKeysIterator() + { + return new \ArrayIterator($this->get(self::MESSAGEKEYS)); + } + + /** + * Returns element from 'messageKeys' list at given offset. + * + * @param int $offset Position in list + * + * @return Textsecure_SessionStructure_Chain_MessageKey + */ + public function getMessageKeysAt($offset) + { + return $this->get(self::MESSAGEKEYS, $offset); + } + + /** + * Returns count of 'messageKeys' list. + * + * @return int + */ + public function getMessageKeysCount() + { + return $this->count(self::MESSAGEKEYS); + } +} + +/** + * PendingKeyExchange message embedded in SessionStructure message. + */ +class Textsecure_SessionStructure_PendingKeyExchange extends \ProtobufMessage +{ + /* Field index constants */ + const SEQUENCE = 1; + const LOCALBASEKEY = 2; + const LOCALBASEKEYPRIVATE = 3; + const LOCALRATCHETKEY = 4; + const LOCALRATCHETKEYPRIVATE = 5; + const LOCALIDENTITYKEY = 7; + const LOCALIDENTITYKEYPRIVATE = 8; + + /* @var array Field descriptors */ + protected static $fields = [ + self::SEQUENCE => [ + 'name' => 'sequence', + 'required' => false, + 'type' => 5, + ], + self::LOCALBASEKEY => [ + 'name' => 'localBaseKey', + 'required' => false, + 'type' => 7, + ], + self::LOCALBASEKEYPRIVATE => [ + 'name' => 'localBaseKeyPrivate', + 'required' => false, + 'type' => 7, + ], + self::LOCALRATCHETKEY => [ + 'name' => 'localRatchetKey', + 'required' => false, + 'type' => 7, + ], + self::LOCALRATCHETKEYPRIVATE => [ + 'name' => 'localRatchetKeyPrivate', + 'required' => false, + 'type' => 7, + ], + self::LOCALIDENTITYKEY => [ + 'name' => 'localIdentityKey', + 'required' => false, + 'type' => 7, + ], + self::LOCALIDENTITYKEYPRIVATE => [ + 'name' => 'localIdentityKeyPrivate', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::SEQUENCE] = null; + $this->values[self::LOCALBASEKEY] = null; + $this->values[self::LOCALBASEKEYPRIVATE] = null; + $this->values[self::LOCALRATCHETKEY] = null; + $this->values[self::LOCALRATCHETKEYPRIVATE] = null; + $this->values[self::LOCALIDENTITYKEY] = null; + $this->values[self::LOCALIDENTITYKEYPRIVATE] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'sequence' property. + * + * @param int $value Property value + * + * @return null + */ + public function setSequence($value) + { + return $this->set(self::SEQUENCE, $value); + } + + /** + * Returns value of 'sequence' property. + * + * @return int + */ + public function getSequence() + { + return $this->get(self::SEQUENCE); + } + + /** + * Sets value of 'localBaseKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setLocalBaseKey($value) + { + return $this->set(self::LOCALBASEKEY, $value); + } + + /** + * Returns value of 'localBaseKey' property. + * + * @return string + */ + public function getLocalBaseKey() + { + return $this->get(self::LOCALBASEKEY); + } + + /** + * Sets value of 'localBaseKeyPrivate' property. + * + * @param string $value Property value + * + * @return null + */ + public function setLocalBaseKeyPrivate($value) + { + return $this->set(self::LOCALBASEKEYPRIVATE, $value); + } + + /** + * Returns value of 'localBaseKeyPrivate' property. + * + * @return string + */ + public function getLocalBaseKeyPrivate() + { + return $this->get(self::LOCALBASEKEYPRIVATE); + } + + /** + * Sets value of 'localRatchetKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setLocalRatchetKey($value) + { + return $this->set(self::LOCALRATCHETKEY, $value); + } + + /** + * Returns value of 'localRatchetKey' property. + * + * @return string + */ + public function getLocalRatchetKey() + { + return $this->get(self::LOCALRATCHETKEY); + } + + /** + * Sets value of 'localRatchetKeyPrivate' property. + * + * @param string $value Property value + * + * @return null + */ + public function setLocalRatchetKeyPrivate($value) + { + return $this->set(self::LOCALRATCHETKEYPRIVATE, $value); + } + + /** + * Returns value of 'localRatchetKeyPrivate' property. + * + * @return string + */ + public function getLocalRatchetKeyPrivate() + { + return $this->get(self::LOCALRATCHETKEYPRIVATE); + } + + /** + * Sets value of 'localIdentityKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setLocalIdentityKey($value) + { + return $this->set(self::LOCALIDENTITYKEY, $value); + } + + /** + * Returns value of 'localIdentityKey' property. + * + * @return string + */ + public function getLocalIdentityKey() + { + return $this->get(self::LOCALIDENTITYKEY); + } + + /** + * Sets value of 'localIdentityKeyPrivate' property. + * + * @param string $value Property value + * + * @return null + */ + public function setLocalIdentityKeyPrivate($value) + { + return $this->set(self::LOCALIDENTITYKEYPRIVATE, $value); + } + + /** + * Returns value of 'localIdentityKeyPrivate' property. + * + * @return string + */ + public function getLocalIdentityKeyPrivate() + { + return $this->get(self::LOCALIDENTITYKEYPRIVATE); + } +} + +/** + * PendingPreKey message embedded in SessionStructure message. + */ +class Textsecure_SessionStructure_PendingPreKey extends \ProtobufMessage +{ + /* Field index constants */ + const PREKEYID = 1; + const SIGNEDPREKEYID = 3; + const BASEKEY = 2; + + /* @var array Field descriptors */ + protected static $fields = [ + self::PREKEYID => [ + 'name' => 'preKeyId', + 'required' => false, + 'type' => 5, + ], + self::SIGNEDPREKEYID => [ + 'name' => 'signedPreKeyId', + 'required' => false, + 'type' => 5, + ], + self::BASEKEY => [ + 'name' => 'baseKey', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::PREKEYID] = null; + $this->values[self::SIGNEDPREKEYID] = null; + $this->values[self::BASEKEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'preKeyId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setPreKeyId($value) + { + return $this->set(self::PREKEYID, $value); + } + + /** + * Returns value of 'preKeyId' property. + * + * @return int + */ + public function getPreKeyId() + { + return $this->get(self::PREKEYID); + } + + /** + * Sets value of 'signedPreKeyId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setSignedPreKeyId($value) + { + return $this->set(self::SIGNEDPREKEYID, $value); + } + + /** + * Returns value of 'signedPreKeyId' property. + * + * @return int + */ + public function getSignedPreKeyId() + { + return $this->get(self::SIGNEDPREKEYID); + } + + /** + * Sets value of 'baseKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setBaseKey($value) + { + return $this->set(self::BASEKEY, $value); + } + + /** + * Returns value of 'baseKey' property. + * + * @return string + */ + public function getBaseKey() + { + return $this->get(self::BASEKEY); + } +} + +/** + * SessionStructure message. + */ +class Textsecure_SessionStructure extends \ProtobufMessage +{ + /* Field index constants */ + const SESSIONVERSION = 1; + const LOCALIDENTITYPUBLIC = 2; + const REMOTEIDENTITYPUBLIC = 3; + const ROOTKEY = 4; + const PREVIOUSCOUNTER = 5; + const SENDERCHAIN = 6; + const RECEIVERCHAINS = 7; + const PENDINGKEYEXCHANGE = 8; + const PENDINGPREKEY = 9; + const REMOTEREGISTRATIONID = 10; + const LOCALREGISTRATIONID = 11; + const NEEDSREFRESH = 12; + const ALICEBASEKEY = 13; + + /* @var array Field descriptors */ + protected static $fields = [ + self::SESSIONVERSION => [ + 'name' => 'sessionVersion', + 'required' => false, + 'type' => 5, + ], + self::LOCALIDENTITYPUBLIC => [ + 'name' => 'localIdentityPublic', + 'required' => false, + 'type' => 7, + ], + self::REMOTEIDENTITYPUBLIC => [ + 'name' => 'remoteIdentityPublic', + 'required' => false, + 'type' => 7, + ], + self::ROOTKEY => [ + 'name' => 'rootKey', + 'required' => false, + 'type' => 7, + ], + self::PREVIOUSCOUNTER => [ + 'name' => 'previousCounter', + 'required' => false, + 'type' => 5, + ], + self::SENDERCHAIN => [ + 'name' => 'senderChain', + 'required' => false, + 'type' => 'Textsecure_SessionStructure_Chain', + ], + self::RECEIVERCHAINS => [ + 'name' => 'receiverChains', + 'repeated' => true, + 'type' => 'Textsecure_SessionStructure_Chain', + ], + self::PENDINGKEYEXCHANGE => [ + 'name' => 'pendingKeyExchange', + 'required' => false, + 'type' => 'Textsecure_SessionStructure_PendingKeyExchange', + ], + self::PENDINGPREKEY => [ + 'name' => 'pendingPreKey', + 'required' => false, + 'type' => 'Textsecure_SessionStructure_PendingPreKey', + ], + self::REMOTEREGISTRATIONID => [ + 'name' => 'remoteRegistrationId', + 'required' => false, + 'type' => 5, + ], + self::LOCALREGISTRATIONID => [ + 'name' => 'localRegistrationId', + 'required' => false, + 'type' => 5, + ], + self::NEEDSREFRESH => [ + 'name' => 'needsRefresh', + 'required' => false, + 'type' => 8, + ], + self::ALICEBASEKEY => [ + 'name' => 'aliceBaseKey', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::SESSIONVERSION] = null; + $this->values[self::LOCALIDENTITYPUBLIC] = null; + $this->values[self::REMOTEIDENTITYPUBLIC] = null; + $this->values[self::ROOTKEY] = null; + $this->values[self::PREVIOUSCOUNTER] = null; + $this->values[self::SENDERCHAIN] = null; + $this->values[self::RECEIVERCHAINS] = []; + $this->values[self::PENDINGKEYEXCHANGE] = null; + $this->values[self::PENDINGPREKEY] = null; + $this->values[self::REMOTEREGISTRATIONID] = null; + $this->values[self::LOCALREGISTRATIONID] = null; + $this->values[self::NEEDSREFRESH] = null; + $this->values[self::ALICEBASEKEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'sessionVersion' property. + * + * @param int $value Property value + * + * @return null + */ + public function setSessionVersion($value) + { + return $this->set(self::SESSIONVERSION, $value); + } + + /** + * Returns value of 'sessionVersion' property. + * + * @return int + */ + public function getSessionVersion() + { + return $this->get(self::SESSIONVERSION); + } + + /** + * Sets value of 'localIdentityPublic' property. + * + * @param string $value Property value + * + * @return null + */ + public function setLocalIdentityPublic($value) + { + return $this->set(self::LOCALIDENTITYPUBLIC, $value); + } + + /** + * Returns value of 'localIdentityPublic' property. + * + * @return string + */ + public function getLocalIdentityPublic() + { + return $this->get(self::LOCALIDENTITYPUBLIC); + } + + /** + * Sets value of 'remoteIdentityPublic' property. + * + * @param string $value Property value + * + * @return null + */ + public function setRemoteIdentityPublic($value) + { + return $this->set(self::REMOTEIDENTITYPUBLIC, $value); + } + + /** + * Returns value of 'remoteIdentityPublic' property. + * + * @return string + */ + public function getRemoteIdentityPublic() + { + return $this->get(self::REMOTEIDENTITYPUBLIC); + } + + /** + * Sets value of 'rootKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setRootKey($value) + { + return $this->set(self::ROOTKEY, $value); + } + + /** + * Returns value of 'rootKey' property. + * + * @return string + */ + public function getRootKey() + { + return $this->get(self::ROOTKEY); + } + + /** + * Sets value of 'previousCounter' property. + * + * @param int $value Property value + * + * @return null + */ + public function setPreviousCounter($value) + { + return $this->set(self::PREVIOUSCOUNTER, $value); + } + + /** + * Returns value of 'previousCounter' property. + * + * @return int + */ + public function getPreviousCounter() + { + return $this->get(self::PREVIOUSCOUNTER); + } + + /** + * Sets value of 'senderChain' property. + * + * @param Textsecure_SessionStructure_Chain $value Property value + * + * @return null + */ + public function setSenderChain(Textsecure_SessionStructure_Chain $value) + { + return $this->set(self::SENDERCHAIN, $value); + } + + /** + * Returns value of 'senderChain' property. + * + * @return Textsecure_SessionStructure_Chain + */ + public function getSenderChain() + { + return $this->get(self::SENDERCHAIN); + } + + /** + * Appends value to 'receiverChains' list. + * + * @param Textsecure_SessionStructure_Chain $value Value to append + * + * @return null + */ + public function appendReceiverChains(Textsecure_SessionStructure_Chain $value) + { + return $this->append(self::RECEIVERCHAINS, $value); + } + + /** + * Clears 'receiverChains' list. + * + * @return null + */ + public function clearReceiverChains() + { + return $this->clear(self::RECEIVERCHAINS); + } + + /** + * Returns 'receiverChains' list. + * + * @return Textsecure_SessionStructure_Chain[] + */ + public function getReceiverChains() + { + return $this->get(self::RECEIVERCHAINS); + } + + /** + * Returns 'receiverChains' iterator. + * + * @return ArrayIterator + */ + public function getReceiverChainsIterator() + { + return new \ArrayIterator($this->get(self::RECEIVERCHAINS)); + } + + /** + * Returns element from 'receiverChains' list at given offset. + * + * @param int $offset Position in list + * + * @return Textsecure_SessionStructure_Chain + */ + public function getReceiverChainsAt($offset) + { + return $this->get(self::RECEIVERCHAINS, $offset); + } + + /** + * Returns count of 'receiverChains' list. + * + * @return int + */ + public function getReceiverChainsCount() + { + return $this->count(self::RECEIVERCHAINS); + } + + /** + * Sets value of 'pendingKeyExchange' property. + * + * @param Textsecure_SessionStructure_PendingKeyExchange $value Property value + * + * @return null + */ + public function setPendingKeyExchange(Textsecure_SessionStructure_PendingKeyExchange $value) + { + return $this->set(self::PENDINGKEYEXCHANGE, $value); + } + + /** + * Returns value of 'pendingKeyExchange' property. + * + * @return Textsecure_SessionStructure_PendingKeyExchange + */ + public function getPendingKeyExchange() + { + return $this->get(self::PENDINGKEYEXCHANGE); + } + + /** + * Sets value of 'pendingPreKey' property. + * + * @param Textsecure_SessionStructure_PendingPreKey $value Property value + * + * @return null + */ + public function setPendingPreKey(Textsecure_SessionStructure_PendingPreKey $value) + { + return $this->set(self::PENDINGPREKEY, $value); + } + + /** + * Returns value of 'pendingPreKey' property. + * + * @return Textsecure_SessionStructure_PendingPreKey + */ + public function getPendingPreKey() + { + return $this->get(self::PENDINGPREKEY); + } + + public function clearPendingPreKey() + { + $this->values[self::PENDINGPREKEY] = null; + } + + /** + * Sets value of 'remoteRegistrationId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setRemoteRegistrationId($value) + { + return $this->set(self::REMOTEREGISTRATIONID, $value); + } + + /** + * Returns value of 'remoteRegistrationId' property. + * + * @return int + */ + public function getRemoteRegistrationId() + { + return $this->get(self::REMOTEREGISTRATIONID); + } + + /** + * Sets value of 'localRegistrationId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setLocalRegistrationId($value) + { + return $this->set(self::LOCALREGISTRATIONID, $value); + } + + /** + * Returns value of 'localRegistrationId' property. + * + * @return int + */ + public function getLocalRegistrationId() + { + return $this->get(self::LOCALREGISTRATIONID); + } + + /** + * Sets value of 'needsRefresh' property. + * + * @param bool $value Property value + * + * @return null + */ + public function setNeedsRefresh($value) + { + return $this->set(self::NEEDSREFRESH, $value); + } + + /** + * Returns value of 'needsRefresh' property. + * + * @return bool + */ + public function getNeedsRefresh() + { + return $this->get(self::NEEDSREFRESH); + } + + /** + * Sets value of 'aliceBaseKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setAliceBaseKey($value) + { + return $this->set(self::ALICEBASEKEY, $value); + } + + /** + * Returns value of 'aliceBaseKey' property. + * + * @return string + */ + public function getAliceBaseKey() + { + return $this->get(self::ALICEBASEKEY); + } +} + +/** + * RecordStructure message. + */ +class Textsecure_RecordStructure extends \ProtobufMessage +{ + /* Field index constants */ + const CURRENTSESSION = 1; + const PREVIOUSSESSIONS = 2; + + /* @var array Field descriptors */ + protected static $fields = [ + self::CURRENTSESSION => [ + 'name' => 'currentSession', + 'required' => false, + 'type' => 'Textsecure_SessionStructure', + ], + self::PREVIOUSSESSIONS => [ + 'name' => 'previousSessions', + 'repeated' => true, + 'type' => 'Textsecure_SessionStructure', + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::CURRENTSESSION] = null; + $this->values[self::PREVIOUSSESSIONS] = []; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'currentSession' property. + * + * @param Textsecure_SessionStructure $value Property value + * + * @return null + */ + public function setCurrentSession(Textsecure_SessionStructure $value) + { + return $this->set(self::CURRENTSESSION, $value); + } + + /** + * Returns value of 'currentSession' property. + * + * @return Textsecure_SessionStructure + */ + public function getCurrentSession() + { + return $this->get(self::CURRENTSESSION); + } + + /** + * Appends value to 'previousSessions' list. + * + * @param Textsecure_SessionStructure $value Value to append + * + * @return null + */ + public function appendPreviousSessions(Textsecure_SessionStructure $value) + { + return $this->append(self::PREVIOUSSESSIONS, $value); + } + + /** + * Clears 'previousSessions' list. + * + * @return null + */ + public function clearPreviousSessions() + { + return $this->clear(self::PREVIOUSSESSIONS); + } + + /** + * Returns 'previousSessions' list. + * + * @return Textsecure_SessionStructure[] + */ + public function getPreviousSessions() + { + return $this->get(self::PREVIOUSSESSIONS); + } + + /** + * Returns 'previousSessions' iterator. + * + * @return ArrayIterator + */ + public function getPreviousSessionsIterator() + { + return new \ArrayIterator($this->get(self::PREVIOUSSESSIONS)); + } + + /** + * Returns element from 'previousSessions' list at given offset. + * + * @param int $offset Position in list + * + * @return Textsecure_SessionStructure + */ + public function getPreviousSessionsAt($offset) + { + return $this->get(self::PREVIOUSSESSIONS, $offset); + } + + /** + * Returns count of 'previousSessions' list. + * + * @return int + */ + public function getPreviousSessionsCount() + { + return $this->count(self::PREVIOUSSESSIONS); + } +} + +/** + * PreKeyRecordStructure message. + */ +class Textsecure_PreKeyRecordStructure extends \ProtobufMessage +{ + /* Field index constants */ + const ID = 1; + const PUBLICKEY = 2; + const PRIVATEKEY = 3; + + /* @var array Field descriptors */ + protected static $fields = [ + self::ID => [ + 'name' => 'id', + 'required' => false, + 'type' => 5, + ], + self::PUBLICKEY => [ + 'name' => 'publicKey', + 'required' => false, + 'type' => 7, + ], + self::PRIVATEKEY => [ + 'name' => 'privateKey', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::ID] = null; + $this->values[self::PUBLICKEY] = null; + $this->values[self::PRIVATEKEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'id' property. + * + * @param int $value Property value + * + * @return null + */ + public function setId($value) + { + return $this->set(self::ID, $value); + } + + /** + * Returns value of 'id' property. + * + * @return int + */ + public function getId() + { + return $this->get(self::ID); + } + + /** + * Sets value of 'publicKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPublicKey($value) + { + return $this->set(self::PUBLICKEY, $value); + } + + /** + * Returns value of 'publicKey' property. + * + * @return string + */ + public function getPublicKey() + { + return $this->get(self::PUBLICKEY); + } + + /** + * Sets value of 'privateKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPrivateKey($value) + { + return $this->set(self::PRIVATEKEY, $value); + } + + /** + * Returns value of 'privateKey' property. + * + * @return string + */ + public function getPrivateKey() + { + return $this->get(self::PRIVATEKEY); + } +} + +/** + * SignedPreKeyRecordStructure message. + */ +class Textsecure_SignedPreKeyRecordStructure extends \ProtobufMessage +{ + /* Field index constants */ + const ID = 1; + const PUBLICKEY = 2; + const PRIVATEKEY = 3; + const SIGNATURE = 4; + const TIMESTAMP = 5; + + /* @var array Field descriptors */ + protected static $fields = [ + self::ID => [ + 'name' => 'id', + 'required' => false, + 'type' => 5, + ], + self::PUBLICKEY => [ + 'name' => 'publicKey', + 'required' => false, + 'type' => 7, + ], + self::PRIVATEKEY => [ + 'name' => 'privateKey', + 'required' => false, + 'type' => 7, + ], + self::SIGNATURE => [ + 'name' => 'signature', + 'required' => false, + 'type' => 7, + ], + self::TIMESTAMP => [ + 'name' => 'timestamp', + 'required' => false, + 'type' => 3, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::ID] = null; + $this->values[self::PUBLICKEY] = null; + $this->values[self::PRIVATEKEY] = null; + $this->values[self::SIGNATURE] = null; + $this->values[self::TIMESTAMP] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'id' property. + * + * @param int $value Property value + * + * @return null + */ + public function setId($value) + { + return $this->set(self::ID, $value); + } + + /** + * Returns value of 'id' property. + * + * @return int + */ + public function getId() + { + return $this->get(self::ID); + } + + /** + * Sets value of 'publicKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPublicKey($value) + { + return $this->set(self::PUBLICKEY, $value); + } + + /** + * Returns value of 'publicKey' property. + * + * @return string + */ + public function getPublicKey() + { + return $this->get(self::PUBLICKEY); + } + + /** + * Sets value of 'privateKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPrivateKey($value) + { + return $this->set(self::PRIVATEKEY, $value); + } + + /** + * Returns value of 'privateKey' property. + * + * @return string + */ + public function getPrivateKey() + { + return $this->get(self::PRIVATEKEY); + } + + /** + * Sets value of 'signature' property. + * + * @param string $value Property value + * + * @return null + */ + public function setSignature($value) + { + return $this->set(self::SIGNATURE, $value); + } + + /** + * Returns value of 'signature' property. + * + * @return string + */ + public function getSignature() + { + return $this->get(self::SIGNATURE); + } + + /** + * Sets value of 'timestamp' property. + * + * @param int $value Property value + * + * @return null + */ + public function setTimestamp($value) + { + return $this->set(self::TIMESTAMP, $value); + } + + /** + * Returns value of 'timestamp' property. + * + * @return int + */ + public function getTimestamp() + { + return $this->get(self::TIMESTAMP); + } +} + +/** + * IdentityKeyPairStructure message. + */ +class Textsecure_IdentityKeyPairStructure extends \ProtobufMessage +{ + /* Field index constants */ + const PUBLICKEY = 1; + const PRIVATEKEY = 2; + + /* @var array Field descriptors */ + protected static $fields = [ + self::PUBLICKEY => [ + 'name' => 'publicKey', + 'required' => false, + 'type' => 7, + ], + self::PRIVATEKEY => [ + 'name' => 'privateKey', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::PUBLICKEY] = null; + $this->values[self::PRIVATEKEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'publicKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPublicKey($value) + { + return $this->set(self::PUBLICKEY, $value); + } + + /** + * Returns value of 'publicKey' property. + * + * @return string + */ + public function getPublicKey() + { + return $this->get(self::PUBLICKEY); + } + + /** + * Sets value of 'privateKey' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPrivateKey($value) + { + return $this->set(self::PRIVATEKEY, $value); + } + + /** + * Returns value of 'privateKey' property. + * + * @return string + */ + public function getPrivateKey() + { + return $this->get(self::PRIVATEKEY); + } +} + +/** + * SenderChainKey message embedded in SenderKeyStateStructure message. + */ +class Textsecure_SenderKeyStateStructure_SenderChainKey extends \ProtobufMessage +{ + /* Field index constants */ + const ITERATION = 1; + const SEED = 2; + + /* @var array Field descriptors */ + protected static $fields = [ + self::ITERATION => [ + 'name' => 'iteration', + 'required' => false, + 'type' => 5, + ], + self::SEED => [ + 'name' => 'seed', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::ITERATION] = null; + $this->values[self::SEED] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'iteration' property. + * + * @param int $value Property value + * + * @return null + */ + public function setIteration($value) + { + return $this->set(self::ITERATION, $value); + } + + /** + * Returns value of 'iteration' property. + * + * @return int + */ + public function getIteration() + { + return $this->get(self::ITERATION); + } + + /** + * Sets value of 'seed' property. + * + * @param string $value Property value + * + * @return null + */ + public function setSeed($value) + { + return $this->set(self::SEED, $value); + } + + /** + * Returns value of 'seed' property. + * + * @return string + */ + public function getSeed() + { + return $this->get(self::SEED); + } +} + +/** + * SenderMessageKey message embedded in SenderKeyStateStructure message. + */ +class Textsecure_SenderKeyStateStructure_SenderMessageKey extends \ProtobufMessage +{ + /* Field index constants */ + const ITERATION = 1; + const SEED = 2; + + /* @var array Field descriptors */ + protected static $fields = [ + self::ITERATION => [ + 'name' => 'iteration', + 'required' => false, + 'type' => 5, + ], + self::SEED => [ + 'name' => 'seed', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::ITERATION] = null; + $this->values[self::SEED] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'iteration' property. + * + * @param int $value Property value + * + * @return null + */ + public function setIteration($value) + { + return $this->set(self::ITERATION, $value); + } + + /** + * Returns value of 'iteration' property. + * + * @return int + */ + public function getIteration() + { + return $this->get(self::ITERATION); + } + + /** + * Sets value of 'seed' property. + * + * @param string $value Property value + * + * @return null + */ + public function setSeed($value) + { + return $this->set(self::SEED, $value); + } + + /** + * Returns value of 'seed' property. + * + * @return string + */ + public function getSeed() + { + return $this->get(self::SEED); + } +} + +/** + * SenderSigningKey message embedded in SenderKeyStateStructure message. + */ +class Textsecure_SenderKeyStateStructure_SenderSigningKey extends \ProtobufMessage +{ + /* Field index constants */ + const _PUBLIC = 1; + const _PRIVATE = 2; + + /* @var array Field descriptors */ + protected static $fields = [ + self::_PUBLIC => [ + 'name' => 'public', + 'required' => false, + 'type' => 7, + ], + self::_PRIVATE => [ + 'name' => 'private', + 'required' => false, + 'type' => 7, + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::_PUBLIC] = null; + $this->values[self::_PRIVATE] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'public' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPublic($value) + { + return $this->set(self::_PUBLIC, $value); + } + + /** + * Returns value of 'public' property. + * + * @return string + */ + public function getPublic() + { + return $this->get(self::_PUBLIC); + } + + /** + * Sets value of 'private' property. + * + * @param string $value Property value + * + * @return null + */ + public function setPrivate($value) + { + return $this->set(self::_PRIVATE, $value); + } + + /** + * Returns value of 'private' property. + * + * @return string + */ + public function getPrivate() + { + return $this->get(self::_PRIVATE); + } +} + +/** + * SenderKeyStateStructure message. + */ +class Textsecure_SenderKeyStateStructure extends \ProtobufMessage +{ + /* Field index constants */ + const SENDERKEYID = 1; + const SENDERCHAINKEY = 2; + const SENDERSIGNINGKEY = 3; + const SENDERMESSAGEKEYS = 4; + + /* @var array Field descriptors */ + protected static $fields = [ + self::SENDERKEYID => [ + 'name' => 'senderKeyId', + 'required' => false, + 'type' => 5, + ], + self::SENDERCHAINKEY => [ + 'name' => 'senderChainKey', + 'required' => false, + 'type' => 'Textsecure_SenderKeyStateStructure_SenderChainKey', + ], + self::SENDERSIGNINGKEY => [ + 'name' => 'senderSigningKey', + 'required' => false, + 'type' => 'Textsecure_SenderKeyStateStructure_SenderSigningKey', + ], + self::SENDERMESSAGEKEYS => [ + 'name' => 'senderMessageKeys', + 'repeated' => true, + 'type' => 'Textsecure_SenderKeyStateStructure_SenderMessageKey', + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::SENDERKEYID] = null; + $this->values[self::SENDERCHAINKEY] = null; + $this->values[self::SENDERSIGNINGKEY] = null; + $this->values[self::SENDERMESSAGEKEYS] = []; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Sets value of 'senderKeyId' property. + * + * @param int $value Property value + * + * @return null + */ + public function setSenderKeyId($value) + { + return $this->set(self::SENDERKEYID, $value); + } + + /** + * Returns value of 'senderKeyId' property. + * + * @return int + */ + public function getSenderKeyId() + { + return $this->get(self::SENDERKEYID); + } + + /** + * Sets value of 'senderChainKey' property. + * + * @param Textsecure_SenderKeyStateStructure_SenderChainKey $value Property value + * + * @return null + */ + public function setSenderChainKey(Textsecure_SenderKeyStateStructure_SenderChainKey $value) + { + return $this->set(self::SENDERCHAINKEY, $value); + } + + /** + * Returns value of 'senderChainKey' property. + * + * @return Textsecure_SenderKeyStateStructure_SenderChainKey + */ + public function getSenderChainKey() + { + return $this->get(self::SENDERCHAINKEY); + } + + /** + * Sets value of 'senderSigningKey' property. + * + * @param Textsecure_SenderKeyStateStructure_SenderSigningKey $value Property value + * + * @return null + */ + public function setSenderSigningKey(Textsecure_SenderKeyStateStructure_SenderSigningKey $value) + { + return $this->set(self::SENDERSIGNINGKEY, $value); + } + + /** + * Returns value of 'senderSigningKey' property. + * + * @return Textsecure_SenderKeyStateStructure_SenderSigningKey + */ + public function getSenderSigningKey() + { + return $this->get(self::SENDERSIGNINGKEY); + } + + /** + * Appends value to 'senderMessageKeys' list. + * + * @param Textsecure_SenderKeyStateStructure_SenderMessageKey $value Value to append + * + * @return null + */ + public function appendSenderMessageKeys(Textsecure_SenderKeyStateStructure_SenderMessageKey $value) + { + return $this->append(self::SENDERMESSAGEKEYS, $value); + } + + /** + * Clears 'senderMessageKeys' list. + * + * @return null + */ + public function clearSenderMessageKeys() + { + return $this->clear(self::SENDERMESSAGEKEYS); + } + + /** + * Returns 'senderMessageKeys' list. + * + * @return Textsecure_SenderKeyStateStructure_SenderMessageKey[] + */ + public function getSenderMessageKeys() + { + return $this->get(self::SENDERMESSAGEKEYS); + } + + /** + * Returns 'senderMessageKeys' iterator. + * + * @return ArrayIterator + */ + public function getSenderMessageKeysIterator() + { + return new \ArrayIterator($this->get(self::SENDERMESSAGEKEYS)); + } + + /** + * Returns element from 'senderMessageKeys' list at given offset. + * + * @param int $offset Position in list + * + * @return Textsecure_SenderKeyStateStructure_SenderMessageKey + */ + public function getSenderMessageKeysAt($offset) + { + return $this->get(self::SENDERMESSAGEKEYS, $offset); + } + + /** + * Returns count of 'senderMessageKeys' list. + * + * @return int + */ + public function getSenderMessageKeysCount() + { + return $this->count(self::SENDERMESSAGEKEYS); + } +} + +/** + * SenderKeyRecordStructure message. + */ +class Textsecure_SenderKeyRecordStructure extends \ProtobufMessage +{ + /* Field index constants */ + const SENDERKEYSTATES = 1; + + /* @var array Field descriptors */ + protected static $fields = [ + self::SENDERKEYSTATES => [ + 'name' => 'senderKeyStates', + 'repeated' => true, + 'type' => 'Textsecure_SenderKeyStateStructure', + ], + ]; + + /** + * Constructs new message container and clears its internal state. + * + * @return null + */ + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::SENDERKEYSTATES] = []; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + /** + * Appends value to 'senderKeyStates' list. + * + * @param Textsecure_SenderKeyStateStructure $value Value to append + * + * @return null + */ + public function appendSenderKeyStates(Textsecure_SenderKeyStateStructure $value) + { + return $this->append(self::SENDERKEYSTATES, $value); + } + + /** + * Clears 'senderKeyStates' list. + * + * @return null + */ + public function clearSenderKeyStates() + { + return $this->clear(self::SENDERKEYSTATES); + } + + /** + * Returns 'senderKeyStates' list. + * + * @return Textsecure_SenderKeyStateStructure[] + */ + public function getSenderKeyStates() + { + return $this->get(self::SENDERKEYSTATES); + } + + /** + * Returns 'senderKeyStates' iterator. + * + * @return ArrayIterator + */ + public function getSenderKeyStatesIterator() + { + return new \ArrayIterator($this->get(self::SENDERKEYSTATES)); + } + + /** + * Returns element from 'senderKeyStates' list at given offset. + * + * @param int $offset Position in list + * + * @return Textsecure_SenderKeyStateStructure + */ + public function getSenderKeyStatesAt($offset) + { + return $this->get(self::SENDERKEYSTATES, $offset); + } + + /** + * Returns count of 'senderKeyStates' list. + * + * @return int + */ + public function getSenderKeyStatesCount() + { + return $this->count(self::SENDERKEYSTATES); + } +} diff --git a/src/libaxolotl-php/tests/groups/inmemorysenderkeystore.php b/src/libaxolotl-php/tests/groups/inmemorysenderkeystore.php new file mode 100644 index 00000000..9a3825af --- /dev/null +++ b/src/libaxolotl-php/tests/groups/inmemorysenderkeystore.php @@ -0,0 +1,28 @@ +store = []; + } + + public function storeSenderKey($senderKeyId, $senderKeyRecord) + { + $this->store[$senderKeyId] = $senderKeyRecord; + } + + public function loadSenderKey($senderKeyId) + { + if (isset($this->store[$senderKeyId])) { + return new SenderKeyRecord($this->store[$senderKeyId]->serialize()); + } + + return new SenderKeyRecord(); + } +} diff --git a/src/libaxolotl-php/tests/groups/test_groupcipher.php b/src/libaxolotl-php/tests/groups/test_groupcipher.php new file mode 100644 index 00000000..9ac1a877 --- /dev/null +++ b/src/libaxolotl-php/tests/groups/test_groupcipher.php @@ -0,0 +1,210 @@ + 230) { + $txt = 'HEX:'.bin2hex($txt); + + return $txt; + } + } + + return $txt; +} +function niceVarDump($obj, $ident = 0) +{ + $data = ''; + $data .= str_repeat(' ', $ident); + $original_ident = $ident; + $toClose = false; + switch (gettype($obj)) { + case 'object': + $vars = (array) $obj; + $data .= gettype($obj).' ('.get_class($obj).') ('.count($vars).") {\n"; + $ident += 2; + foreach ($vars as $key => $var) { + $type = ''; + $k = bin2hex($key); + if (strpos($k, '002a00') === 0) { + $k = str_replace('002a00', '', $k); + $type = ':protected'; + } elseif (strpos($k, bin2hex("\x00".get_class($obj)."\x00")) === 0) { + $k = str_replace(bin2hex("\x00".get_class($obj)."\x00"), '', $k); + $type = ':private'; + } + $k = hex2bin($k); + if (is_subclass_of($obj, 'ProtobufMessage') && $k == 'values') { + $r = new ReflectionClass($obj); + $constants = $r->getConstants(); + $newVar = []; + foreach ($constants as $ckey => $cval) { + if (substr($ckey, 0, 3) != 'PB_') { + $newVar[$ckey] = $var[$cval]; + } + } + $var = $newVar; + } + $data .= str_repeat(' ', $ident)."[$k$type]=>\n".niceVarDump($var, $ident)."\n"; + } + $toClose = true; + break; + case 'array': + $data .= 'array ('.count($obj).") {\n"; + $ident += 2; + foreach ($obj as $key => $val) { + $data .= str_repeat(' ', $ident).'['.(is_int($key) ? $key : "\"$key\"")."]=>\n".niceVarDump($val, $ident)."\n"; + } + $toClose = true; + break; + case 'string': + $data .= 'string "'.parseText($obj)."\"\n"; + break; + case 'NULL': + $data .= gettype($obj); + break; + default: + $data .= gettype($obj).'('.strval($obj).")\n"; + break; + } + if ($toClose) { + $data .= str_repeat(' ', $original_ident)."}\n"; + } + + return $data; +} +class GroupCipherTest extends PHPUnit_Framework_TestCase +{ + public function test_basicEncryptDecrypt() + { + $aliceStore = new InMemorySenderKeyStore(); + $bobStore = new InMemorySenderKeyStore(); + $charlieStore = new InMemorySenderKeyStore(); + + $aliceSessionBuilder = new GroupSessionBuilder($aliceStore); + $bobSessionBuilder = new GroupSessionBuilder($bobStore); + $charlieSessionBuilder = new GroupSessionBuilder($charlieStore); + + $aliceGroupCipher = new GroupCipher($aliceStore, 'groupWithBobInIt'); + $bobGroupCipher = new GroupCipher($bobStore, 'groupWithBobInIt::aliceUserName'); + $charlieGroupCipher = new GroupCipher($charlieStore, 'groupWithBobInIt::aliceUserName'); + + $aliceSenderKey = KeyHelper::generateSenderKey(); + $aliceSenderSigningKey = KeyHelper::generateSenderSigningKey(); + $aliceSenderKeyId = KeyHelper::generateSenderKeyId(); + + $aliceDistributionMessage = $aliceSessionBuilder->process('groupWithBobInIt', $aliceSenderKeyId, 0, + $aliceSenderKey, $aliceSenderSigningKey); + echo niceVarDump($aliceDistributionMessage); + echo niceVarDump($aliceDistributionMessage->serialize()); + echo $aliceDistributionMessage->serialize(); + $bobSessionBuilder->processSender('groupWithBobInIt::aliceUserName', $aliceDistributionMessage); + + $ciphertextFromAlice = $aliceGroupCipher->encrypt('smert ze smert'); + $plaintextFromAlice_Bob = $bobGroupCipher->decrypt($ciphertextFromAlice); + $ciphertextFromAlice_2 = $aliceGroupCipher->encrypt('smert ze smert'); + echo niceVarDump($aliceDistributionMessage); + $charlieSessionBuilder->processSender('groupWithBobInIt::aliceUserName', $aliceDistributionMessage); + $plaintextFromAlice_Charlie = $charlieGroupCipher->decrypt($ciphertextFromAlice_2); + + $this->assertEquals($plaintextFromAlice_Bob, 'smert ze smert'); + $this->assertEquals($plaintextFromAlice_Charlie, 'smert ze smert'); + } + + /* public function test_basicRatchet() + { + $aliceStore = new InMemorySenderKeyStore(); + $bobStore = new InMemorySenderKeyStore(); + + $aliceSessionBuilder = new GroupSessionBuilder($aliceStore); + $bobSessionBuilder = new GroupSessionBuilder($bobStore); + + $aliceGroupCipher = new GroupCipher($aliceStore, "groupWithBobInIt"); + $bobGroupCipher = new GroupCipher($bobStore, "groupWithBobInIt::aliceUserName"); + + $aliceSenderKey = KeyHelper::generateSenderKey(); + $aliceSenderSigningKey = KeyHelper::generateSenderSigningKey(); + $aliceSenderKeyId = KeyHelper::generateSenderKeyId(); + + $aliceDistributionMessage = $aliceSessionBuilder->process("groupWithBobInIt", $aliceSenderKeyId, 0, + $aliceSenderKey, $aliceSenderSigningKey); + + $bobSessionBuilder->processSender("groupWithBobInIt::aliceUserName", $aliceDistributionMessage); + + $ciphertextFromAlice = $aliceGroupCipher->encrypt("smert ze smert"); + $ciphertextFromAlice2 = $aliceGroupCipher->encrypt("smert ze smert2"); + $ciphertextFromAlice3 = $aliceGroupCipher->encrypt("smert ze smert3"); + + $plaintextFromAlice = $bobGroupCipher->decrypt($ciphertextFromAlice); + + try { + $bobGroupCipher->decrypt($ciphertextFromAlice); + throw new AssertionError("Should have ratcheted forward!"); + } catch (DuplicateMessageException $dme) { + #good + } + + $plaintextFromAlice2 = $bobGroupCipher->decrypt($ciphertextFromAlice2); + $plaintextFromAlice3 = $bobGroupCipher->decrypt($ciphertextFromAlice3); + + $this->assertEquals($plaintextFromAlice,"smert ze smert"); + $this->assertEquals($plaintextFromAlice2, "smert ze smert2"); + $this->assertEquals($plaintextFromAlice3, "smert ze smert3"); + + } + public function test_outOfOrder() + { + + $aliceStore = new InMemorySenderKeyStore(); + $bobStore = new InMemorySenderKeyStore(); + + $aliceSessionBuilder = new GroupSessionBuilder($aliceStore); + $bobSessionBuilder = new GroupSessionBuilder($bobStore); + + $aliceGroupCipher = new GroupCipher($aliceStore, "groupWithBobInIt"); + $bobGroupCipher = new GroupCipher($bobStore, "groupWithBobInIt::aliceUserName"); + + $aliceSenderKey = KeyHelper::generateSenderKey(); + $aliceSenderSigningKey = KeyHelper::generateSenderSigningKey(); + $aliceSenderKeyId = KeyHelper::generateSenderKeyId(); + + $aliceDistributionMessage = $aliceSessionBuilder->process("groupWithBobInIt", $aliceSenderKeyId, 0, + $aliceSenderKey, $aliceSenderSigningKey); + + $bobSessionBuilder->processSender("groupWithBobInIt::aliceUserName", $aliceDistributionMessage); + + $ciphertexts = []; + for ($i = 0; $i < 100; $i++) + $ciphertexts[] = $aliceGroupCipher->encrypt("up the punks"); + while (count($ciphertexts) > 0) + { + $index = KeyHelper::getRandomSequence(2147483647) % count($ciphertexts); + $elements = array_splice($ciphertexts,$index,1); + $ciphertext = $elements[0]; + $plaintext = $bobGroupCipher->decrypt($ciphertext); + $this->assertEquals($plaintext, "up the punks"); + } + } + + public function test_encryptNoSession() + { + $aliceStore = new InMemorySenderKeyStore(); + $aliceGroupCipher = new GroupCipher($aliceStore, "groupWithBobInIt"); + try + { + $aliceGroupCipher->encrypt("up the punks"); + throw new AssertionError("Should have failed!"); + } + catch (NoSessionException $nse) + { + # good + } + }*/ +} diff --git a/src/libaxolotl-php/tests/inmemoryaxolotlstore.php b/src/libaxolotl-php/tests/inmemoryaxolotlstore.php new file mode 100755 index 00000000..feafb0cd --- /dev/null +++ b/src/libaxolotl-php/tests/inmemoryaxolotlstore.php @@ -0,0 +1,117 @@ +identityKeyStore = new InMemoryIdentityKeyStore(); + $this->preKeyStore = new InMemoryPreKeyStore(); + $this->signedPreKeyStore = new InMemorySignedPreKeyStore(); + $this->sessionStore = new InMemorySessionStore(); + } + + public function getIdentityKeyPair() + { + return $this->identityKeyStore->getIdentityKeyPair(); + } + + public function getLocalRegistrationId() + { + return $this->identityKeyStore->getLocalRegistrationId(); + } + + public function saveIdentity($recepientId, $identityKey) + { + $this->identityKeyStore->saveIdentity($recepientId, $identityKey); + } + + public function isTrustedIdentity($recepientId, $identityKey) + { + return $this->identityKeyStore->isTrustedIdentity($recepientId, $identityKey); + } + + public function loadPreKey($preKeyId) + { + return $this->preKeyStore->loadPreKey($preKeyId); + } + + public function storePreKey($preKeyId, $preKeyRecord) + { + $this->preKeyStore->storePreKey($preKeyId, $preKeyRecord); + } + + public function containsPreKey($preKeyId) + { + return $this->preKeyStore->containsPreKey($preKeyId); + } + + public function removePreKey($preKeyId) + { + $this->preKeyStore->removePreKey($preKeyId); + } + + public function loadSession($recepientId, $deviceId) + { + return $this->sessionStore->loadSession($recepientId, $deviceId); + } + + public function getSubDeviceSessions($recepientId) + { + return $this->sessionStore->getSubDeviceSessions($recepientId); + } + + public function storeSession($recepientId, $deviceId, $sessionRecord) + { + $this->sessionStore->storeSession($recepientId, $deviceId, $sessionRecord); + } + + public function containsSession($recepientId, $deviceId) + { + return $this->sessionStore->containsSession($recepientId, $deviceId); + } + + public function deleteSession($recepientId, $deviceId) + { + $this->sessionStore->deleteSession($recepientId, $deviceId); + } + + public function deleteAllSessions($recepientId) + { + $this->sessionStore->deleteAllSessions($recepientId); + } + + public function loadSignedPreKey($signedPreKeyId) + { + return $this->signedPreKeyStore->loadSignedPreKey($signedPreKeyId); + } + + public function loadSignedPreKeys() + { + return $this->signedPreKeyStore->loadSignedPreKeys(); + } + + public function storeSignedPreKey($signedPreKeyId, $signedPreKeyRecord) + { + $this->signedPreKeyStore->storeSignedPreKey($signedPreKeyId, $signedPreKeyRecord); + } + + public function containsSignedPreKey($signedPreKeyId) + { + return $this->signedPreKeyStore->containsSignedPreKey($signedPreKeyId); + } + + public function removeSignedPreKey($signedPreKeyId) + { + return $this->signedPreKeyStore->containsSignedPreKey(); + } +} diff --git a/src/libaxolotl-php/tests/inmemoryidentitykeystore.php b/src/libaxolotl-php/tests/inmemoryidentitykeystore.php new file mode 100755 index 00000000..58a27f62 --- /dev/null +++ b/src/libaxolotl-php/tests/inmemoryidentitykeystore.php @@ -0,0 +1,51 @@ +trustedKeys = []; + $identityKeyPairKeys = Curve::generateKeyPair(); + $this->identityKeyPair = new IdentityKeyPair(new IdentityKey($identityKeyPairKeys->getPublicKey()), + $identityKeyPairKeys->getPrivateKey()); + $this->localRegistrationId = KeyHelper::generateRegistrationId(); + } + + public function getIdentityKeyPair() + { + return $this->identityKeyPair; + } + + public function getLocalRegistrationId() + { + return $this->localRegistrationId; + } + + public function saveIdentity($recepientId, $identityKey) + { + $this->trustedKeys[$recepientId] = $identityKey; + } + + public function isTrustedIdentity($recepientId, $identityKey) + { + if (!isset($this->trustedKeys[$recepientId])) { + return true; + } + + return $this->trustedKeys[$recepientId] == $identityKey; + } +} diff --git a/src/libaxolotl-php/tests/inmemoryprekeystore.php b/src/libaxolotl-php/tests/inmemoryprekeystore.php new file mode 100755 index 00000000..d9d40ff9 --- /dev/null +++ b/src/libaxolotl-php/tests/inmemoryprekeystore.php @@ -0,0 +1,44 @@ +store = []; + } + + public function loadPreKey($preKeyId) + { + if (!isset($this->store[$preKeyId])) { + throw new InvalidKeyIdException('No such prekeyRecord!'); + } + + return new PreKeyRecord(null, null, $this->store[$preKeyId]); + } + + public function storePreKey($preKeyId, $preKeyRecord) + { + $this->store[$preKeyId] = $preKeyRecord->serialize(); + } + + public function containsPreKey($preKeyId) + { + return isset($this->store[$preKeyId]); + } + + public function removePreKey($preKeyId) + { + if (isset($this->store[$preKeyId])) { + unset($this->store[$preKeyId]); + } + } +} diff --git a/src/libaxolotl-php/tests/inmemorysessionstore.php b/src/libaxolotl-php/tests/inmemorysessionstore.php new file mode 100755 index 00000000..e2f06cdd --- /dev/null +++ b/src/libaxolotl-php/tests/inmemorysessionstore.php @@ -0,0 +1,73 @@ +sessions = []; + } + + public function loadSession($recepientId, $deviceId) + { + if ($this->containsSession($recepientId, $deviceId)) { + return new SessionRecord(null, $this->sessions[$this->Key($recepientId, $deviceId)]); + } else { + return new SessionRecord(); + } + } + + public function getSubDeviceSessions($recepientId) + { + $deviceIds = []; + foreach (array_keys($this->sessions) as $key) { + $k = $this->SplitKey($key); + if ($k[0] == $recepientId) { + $deviceIds[] = $k[1]; + } + } + + return $deviceIds; + } + + private function Key($recepientId, $deviceId) + { + return $recepientId.'__putaidea__'.$deviceId; + } + + private function SplitKey($key) + { + return explode('__putaidea__', $key); + } + + public function storeSession($recepientId, $deviceId, $sessionRecord) + { + $this->sessions[$this->Key($recepientId, $deviceId)] = $sessionRecord->serialize(); + } + + public function containsSession($recepientId, $deviceId) + { + return isset($this->sessions[$this->Key($recepientId, $deviceId)]); + } + + public function deleteSession($recepientId, $deviceId) + { + unset($this->sessions[$this->Key($recepientId, $deviceId)]); + } + + public function deleteAllSessions($recepientId) + { + foreach (array_keys($this->sessions) as $key) { + $k = $this->SplitKey($key); + if ($k[0] == $recepientId) { + unset($this->sessions[$key]); + } + } + } +} diff --git a/src/libaxolotl-php/tests/inmemorysignedprekeystore.php b/src/libaxolotl-php/tests/inmemorysignedprekeystore.php new file mode 100755 index 00000000..b2304a74 --- /dev/null +++ b/src/libaxolotl-php/tests/inmemorysignedprekeystore.php @@ -0,0 +1,50 @@ +store = []; + } + + public function loadSignedPreKey($signedPreKeyId) + { + if (!isset($this->store[$signedPreKeyId])) { + throw new InvalidKeyIdException('No such signedprekeyrecord! '.$signedPreKeyId); + } + + return new SignedPreKeyRecord(null, null, null, null, $this->store[$signedPreKeyId]); + } + + public function loadSignedPreKeys() + { + $results = []; + foreach ($this->store as $serialized) { + $results[] = new SignedPreKeyRecord(null, null, null, null, $serialized); + } + + return $results; + } + + public function storeSignedPreKey($signedPreKeyId, $signedPreKeyRecord) + { + $this->store[$signedPreKeyId] = $signedPreKeyRecord->serialize(); + } + + public function containsSignedPreKey($signedPreKeyId) + { + return isset($this->store[$signedPreKeyId]); + } + + public function removeSignedPreKey($signedPreKeyId) + { + unset($this->store[$signedPreKeyId]); + } +} diff --git a/src/libaxolotl-php/tests/kdf/test_hkdf.php b/src/libaxolotl-php/tests/kdf/test_hkdf.php new file mode 100755 index 00000000..260fe77c --- /dev/null +++ b/src/libaxolotl-php/tests/kdf/test_hkdf.php @@ -0,0 +1,47 @@ +deriveSecrets($ikm, $info, 42, $salt); + $this->assertEquals($okm, $actualOutput); + } + + public function testVectorLongV3() + { + $ikm = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"; + + $salt = "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"; + + $info = "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"; + + $okm = "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87"; + + $actualOutput = HKDF::createFor(3)->deriveSecrets($ikm, $info, 82, $salt); + $this->assertEquals($okm, $actualOutput); + } + + public function testVectorV2() + { + $ikm = "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"; + + $salt = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c"; + + $info = "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9"; + + $okm = "\x6e\xc2\x55\x6d\x5d\x7b\x1d\x81\xde\xe4\x22\x2a\xd7\x48\x36\x95\xdd\xc9\x8f\x4f\x5f\xab\xc0\xe0\x20\x5d\xc2\xef\x87\x52\xd4\x1e\x04\xe2\xe2\x11\x01\xc6\x8f\xf0\x93\x94\xb8\xad\x0b\xdc\xb9\x60\x9c\xd4\xee\x82\xac\x13\x19\x9b\x4a\xa9\xfd\xa8\x99\xda\xeb\xec"; + + $actualOutput = HKDF::createFor(2)->deriveSecrets($ikm, $info, 64, $salt); + $this->assertEquals($okm, $actualOutput); + } +} diff --git a/src/libaxolotl-php/tests/ratchet/test_chainkey.php b/src/libaxolotl-php/tests/ratchet/test_chainkey.php new file mode 100644 index 00000000..991a573e --- /dev/null +++ b/src/libaxolotl-php/tests/ratchet/test_chainkey.php @@ -0,0 +1,28 @@ +assertEquals($chainKey->getKey(), $seed); + $this->assertEquals($chainKey->getMessageKeys()->getCipherKey(), $messageKey); + $this->assertEquals($chainKey->getMessageKeys()->getMacKey(), $macKey); + $this->assertEquals($chainKey->getNextChainKey()->getKey(), $nextChainKey); + $this->assertEquals($chainKey->getIndex(), 0); + $this->assertEquals($chainKey->getMessageKeys()->getCounter(), 0); + $this->assertEquals($chainKey->getNextChainKey()->getIndex(), 1); + $this->assertEquals($chainKey->getNextChainKey()->getMessageKeys()->getCounter(), 1); + } +} diff --git a/src/libaxolotl-php/tests/ratchet/test_ratchetingsession.php b/src/libaxolotl-php/tests/ratchet/test_ratchetingsession.php new file mode 100755 index 00000000..e71dd299 --- /dev/null +++ b/src/libaxolotl-php/tests/ratchet/test_ratchetingsession.php @@ -0,0 +1,60 @@ +setOurIdentityKey($bobIdentityKey) + ->setOurSignedPreKey($bobBaseKey) + ->setOurRatchetKey($bobEphemeralKey) + ->setOurOneTimePreKey(null) + ->setTheirIdentityKey($aliceIdentityPublicKey) + ->setTheirBaseKey($aliceBasePublicKey) + ->create(); + + $session = new SessionState(); + + RatchetingSession::initializeSessionAsBob($session, 2, $parameters); + $this->assertEquals($session->getLocalIdentityKey(), $bobIdentityKey->getPublicKey()); + $this->assertEquals($session->getRemoteIdentityKey(), $aliceIdentityPublicKey); + $this->assertEquals($session->getSenderChainKey()->getKey(), $senderChain); + } +} diff --git a/src/libaxolotl-php/tests/ratchet/test_rootkey.php b/src/libaxolotl-php/tests/ratchet/test_rootkey.php new file mode 100755 index 00000000..97e3344c --- /dev/null +++ b/src/libaxolotl-php/tests/ratchet/test_rootkey.php @@ -0,0 +1,37 @@ +createChain($bobPublicKey, $aliceKeyPair); + + $nextRootKey = $rootKeyChainKeyPair[0]; + $nextChainKey = $rootKeyChainKeyPair[1]; + + $this->assertEquals($rootKey->getKeyBytes(), $rootKeySeed); + $this->assertEquals($nextRootKey->getKeyBytes(), $nextRoot); + $this->assertEquals($nextChainKey->getKey(), $nextChain); + } +} diff --git a/src/libaxolotl-php/tests/test_curve.php b/src/libaxolotl-php/tests/test_curve.php new file mode 100644 index 00000000..205da1c2 --- /dev/null +++ b/src/libaxolotl-php/tests/test_curve.php @@ -0,0 +1,70 @@ +assertEquals($sharedOne, $shared); + $this->assertEquals($sharedTwo, $shared); + } + + public function testRandomAgreements() + { + for ($i = 0; $i < 50; $i++) { + $alice = Curve::generateKeyPair(); + $bob = Curve::generateKeyPair(); + + $sharedAlice = Curve::calculateAgreement($bob->getPublicKey(), $alice->getPrivateKey()); + $sharedBob = Curve::calculateAgreement($alice->getPublicKey(), $bob->getPrivateKey()); + + $this->assertEquals($sharedAlice, $sharedBob); + } + } + + public function testSignature() + { + $aliceIdentityPrivate = "\xc0\x97\x24\x84\x12\xe5\x8b\xf0\x5d\xf4\x87\x96\x82\x05\x13\x27\x94\x17\x8e\x36\x76\x37\xf5\x81\x8f\x81\xe0\xe6\xce\x73\xe8\x65"; + $aliceIdentityPublic = "\x05\xab\x7e\x71\x7d\x4a\x16\x3b\x7d\x9a\x1d\x80\x71\xdf\xe9\xdc\xf8\xcd\xcd\x1c\xea\x33\x39\xb6\x35\x6b\xe8\x4d\x88\x7e\x32\x2c\x64"; + + $aliceEphemeralPublic = "\x05\xed\xce\x9d\x9c\x41\x5c\xa7\x8c\xb7\x25\x2e\x72\xc2\xc4\xa5\x54\xd3\xeb\x29\x48\x5a\x0e\x1d\x50\x31\x18\xd1\xa8\x2d\x99\xfb\x4a"; + + $aliceSignature = "\x5d\xe8\x8c\xa9\xa8\x9b\x4a\x11\x5d\xa7\x91\x09\xc6\x7c\x9c\x74\x64\xa3\xe4\x18\x02\x74\xf1\xcb\x8c\x63\xc2\x98\x4e\x28\x6d\xfb\xed\xe8\x2d\xeb\x9d\xcd\x9f\xae\x0b\xfb\xb8\x21\x56\x9b\x3d\x90\x01\xbd\x81\x30\xcd\x11\xd4\x86\xce\xf0\x47\xbd\x60\xb8\x6e\x88"; + + $alicePrivateKey = Curve::decodePrivatePoint($aliceIdentityPrivate); + $alicePublicKey = Curve::decodePoint($aliceIdentityPublic, 0); + $aliceEphemeral = Curve::decodePoint($aliceEphemeralPublic, 0); + + if (!Curve::verifySignature($alicePublicKey, $aliceEphemeral->serialize(), $aliceSignature)) { + throw new Exception('Sig verification failed!'); + } + + for ($i = 0; $i < strlen($aliceSignature); $i++) { + $modifiedSignature = (string) $aliceSignature; + + $modifiedSignature[$i] = chr(ord($modifiedSignature[$i]) ^ 0x01); + if (Curve::verifySignature($alicePublicKey, $aliceEphemeral->serialize(), $modifiedSignature)) { + throw new Exception('Sig verification succeeded!'); + } + } + } +} diff --git a/src/libaxolotl-php/tests/test_sessionbuilder.php b/src/libaxolotl-php/tests/test_sessionbuilder.php new file mode 100755 index 00000000..51190343 --- /dev/null +++ b/src/libaxolotl-php/tests/test_sessionbuilder.php @@ -0,0 +1,420 @@ + 230) { + $txt = 'HEX:'.bin2hex($txt); + + return $txt; + } + } + + return $txt; +} +function niceVarDump($obj, $ident = 0) +{ + $data = ''; + $data .= str_repeat(' ', $ident); + $original_ident = $ident; + $toClose = false; + switch (gettype($obj)) { + case 'object': + $vars = (array) $obj; + $data .= gettype($obj).' ('.get_class($obj).') ('.count($vars).") {\n"; + $ident += 2; + foreach ($vars as $key => $var) { + $type = ''; + $k = bin2hex($key); + if (strpos($k, '002a00') === 0) { + $k = str_replace('002a00', '', $k); + $type = ':protected'; + } elseif (strpos($k, bin2hex("\x00".get_class($obj)."\x00")) === 0) { + $k = str_replace(bin2hex("\x00".get_class($obj)."\x00"), '', $k); + $type = ':private'; + } + $k = hex2bin($k); + if (is_subclass_of($obj, 'ProtobufMessage') && $k == 'values') { + $r = new ReflectionClass($obj); + $constants = $r->getConstants(); + $newVar = []; + foreach ($constants as $ckey => $cval) { + if (substr($ckey, 0, 3) != 'PB_') { + $newVar[$ckey] = $var[$cval]; + } + } + $var = $newVar; + } + $data .= str_repeat(' ', $ident)."[$k$type]=>\n".niceVarDump($var, $ident)."\n"; + } + $toClose = true; + break; + case 'array': + $data .= 'array ('.count($obj).") {\n"; + $ident += 2; + foreach ($obj as $key => $val) { + $data .= str_repeat(' ', $ident).'['.(is_int($key) ? $key : "\"$key\"")."]=>\n".niceVarDump($val, $ident)."\n"; + } + $toClose = true; + break; + case 'string': + $data .= 'string "'.parseText($obj)."\"\n"; + break; + case 'NULL': + $data .= gettype($obj); + break; + default: + $data .= gettype($obj).'('.strval($obj).")\n"; + break; + } + if ($toClose) { + $data .= str_repeat(' ', $original_ident)."}\n"; + } + + return $data; +} +class SessionBuilderTest extends PHPUnit_Framework_TestCase +{ + const ALICE_RECIPIENT_ID = 5; + const BOB_RECIPIENT_ID = 2; + + /*public function testBasicPreKeyV2(){ + $aliceStore = new InMemoryAxolotlStore(); + $aliceSessionBuilder = new SessionBuilder($aliceStore, $aliceStore, $aliceStore, $aliceStore, self::BOB_RECIPIENT_ID, 1); + + + $bobStore = new InMemoryAxolotlStore(); + $bobPreKeyPair = Curve::generateKeyPair(); + $bobPreKey = new PreKeyBundle($bobStore->getLocalRegistrationId(), 1, + 31337, $bobPreKeyPair->getPublicKey(), + 0, null, null, + $bobStore->getIdentityKeyPair()->getPublicKey()); + + $aliceSessionBuilder->processPreKeyBundle($bobPreKey); + + $this->assertTrue($aliceStore->containsSession(self::BOB_RECIPIENT_ID, 1)); + $this->assertEquals($aliceStore->loadSession(self::BOB_RECIPIENT_ID, 1)->getSessionState()->getSessionVersion(), 2); + + $originalMessage = "L'homme est condamné à être libre"; + $aliceSessionCipher = new SessionCipher($aliceStore, $aliceStore, $aliceStore, $aliceStore, self::BOB_RECIPIENT_ID, 1); + $outgoingMessage = $aliceSessionCipher->encrypt($originalMessage); + + $this->assertTrue($outgoingMessage->getType() == CiphertextMessage::PREKEY_TYPE); + + $incomingMessage = new PreKeyWhisperMessage(null, null,null,null,null,null,null,$outgoingMessage->serialize()); + $bobStore->storePreKey(31337, new PreKeyRecord($bobPreKey->getPreKeyId(), $bobPreKeyPair)); + + $bobSessionCipher = new SessionCipher($bobStore, $bobStore, $bobStore, $bobStore, self::ALICE_RECIPIENT_ID, 1); + $plaintext = $bobSessionCipher->decryptPkmsg($incomingMessage); + $this->assertTrue($bobStore->containsSession(self::ALICE_RECIPIENT_ID, 1)); + $this->assertTrue($bobStore->loadSession(self::ALICE_RECIPIENT_ID, 1)->getSessionState()->getSessionVersion() == 2); + $this->assertEquals($originalMessage, $plaintext); + + + $bobOutgoingMessage = $bobSessionCipher->encrypt($originalMessage); + $this->assertTrue($bobOutgoingMessage->getType() == CiphertextMessage::WHISPER_TYPE); + + $alicePlaintext = $aliceSessionCipher->decryptMsg($bobOutgoingMessage); + $this->assertEquals($alicePlaintext, $originalMessage); + + $this->runInteraction($aliceStore, $bobStore); + + $aliceStore = new InMemoryAxolotlStore(); + $aliceSessionBuilder = new SessionBuilder($aliceStore, $aliceStore, $aliceStore, $aliceStore, self::BOB_RECIPIENT_ID, 1); + $aliceSessionCipher = new SessionCipher($aliceStore, $aliceStore, $aliceStore, $aliceStore, self::BOB_RECIPIENT_ID, 1); + + $bobPreKeyPair = Curve::generateKeyPair(); + + $bobPreKey = new PreKeyBundle($bobStore->getLocalRegistrationId(), + 1, 31338, $bobPreKeyPair->getPublicKey(), + 0, null, null, $bobStore->getIdentityKeyPair()->getPublicKey()); + + + $bobStore->storePreKey(31338, new PreKeyRecord($bobPreKey->getPreKeyId(), $bobPreKeyPair)); + $aliceSessionBuilder->processPreKeyBundle($bobPreKey); + + $outgoingMessage = $aliceSessionCipher->encrypt($originalMessage); + + try { + $bobSessionCipher->decryptPkmsg(new PreKeyWhisperMessage(null,null,null,null,null,null,null,$outgoingMessage->serialize())); + throw new Exception("shouldn't be trusted!"); + } + catch(Exception $ex){ + $preKeyW = new PreKeyWhisperMessage(null,null,null,null,null,null,null,$outgoingMessage->serialize()); + $bobStore->saveIdentity(self::ALICE_RECIPIENT_ID, $preKeyW->getIdentityKey()); + } + $plaintext = $bobSessionCipher->decryptPkmsg(new PreKeyWhisperMessage(null,null,null,null,null,null,null,$outgoingMessage->serialize())); + $this->assertEquals($plaintext, $originalMessage); + $bobPreKey = new PreKeyBundle($bobStore->getLocalRegistrationId(), 1, + 31337, Curve::generateKeyPair()->getPublicKey(), + 0, null, null, + $aliceStore->getIdentityKeyPair()->getPublicKey()); + try{ + $aliceSessionBuilder->processPreKeyBundle($bobPreKey); + throw new Exception("shouldn't be trusted"); + } + catch(Exception $ex){ + //all ok + } + + return; + + }*/ + /* + public function test_basicPreKeyV3(){ + aliceStore = InMemoryAxolotlStore() + aliceSessionBuilder = SessionBuilder(aliceStore, aliceStore, aliceStore, aliceStore, self::BOB_RECIPIENT_ID, 1) + + bobStore = InMemoryAxolotlStore() + bobPreKeyPair = Curve::generateKeyPair() + bobSignedPreKeyPair = Curve::generateKeyPair() + bobSignedPreKeySignature = Curve::calculateSignature(bobStore.getIdentityKeyPair().getPrivateKey(), + bobSignedPreKeyPair.getPublicKey().serialize()) + + bobPreKey = PreKeyBundle(bobStore.getLocalRegistrationId(), 1, + 31337, bobPreKeyPair.getPublicKey(), + 22, bobSignedPreKeyPair.getPublicKey(), + bobSignedPreKeySignature, + bobStore.getIdentityKeyPair().getPublicKey()) + + aliceSessionBuilder.processPreKeyBundle(bobPreKey) + $this->assertTrue(aliceStore.containsSession(self::BOB_RECIPIENT_ID, 1)) + $this->assertTrue(aliceStore.loadSession(self::BOB_RECIPIENT_ID, 1).getSessionState().getSessionVersion() == 3) + + originalMessage = "L'homme est condamné à être libre" + aliceSessionCipher = SessionCipher(aliceStore, aliceStore, aliceStore, aliceStore, self::BOB_RECIPIENT_ID, 1) + outgoingMessage = aliceSessionCipher.encrypt(originalMessage) + + $this->assertTrue(outgoingMessage.getType() == CiphertextMessage::PREKEY_TYPE) + + incomingMessage = PreKeyWhisperMessage(serialized=outgoingMessage.serialize()) + bobStore.storePreKey(31337, PreKeyRecord(bobPreKey.getPreKeyId(), bobPreKeyPair)) + bobStore.storeSignedPreKey(22, SignedPreKeyRecord(22, int(time.time() * 1000), bobSignedPreKeyPair, bobSignedPreKeySignature)) + + bobSessionCipher = SessionCipher(bobStore, bobStore, bobStore, bobStore, self::ALICE_RECIPIENT_ID, 1) + + plaintext = bobSessionCipher.decryptPkmsg(incomingMessage) + $this->assertEquals(originalMessage, plaintext) + # @@TODO: in callback assertion + # $this->assertFalse(bobStore.containsSession(self::ALICE_RECIPIENT_ID, 1)) + + $this->assertTrue(bobStore.containsSession(self::ALICE_RECIPIENT_ID, 1)) + + $this->assertTrue(bobStore.loadSession(self::ALICE_RECIPIENT_ID, 1).getSessionState().getSessionVersion() == 3) + $this->assertTrue(bobStore.loadSession(self::ALICE_RECIPIENT_ID, 1).getSessionState().getAliceBaseKey() != null) + $this->assertEquals(originalMessage, plaintext) + + bobOutgoingMessage = bobSessionCipher.encrypt(originalMessage) + $this->assertTrue(bobOutgoingMessage.getType() == CiphertextMessage::WHISPER_TYPE) + + alicePlaintext = aliceSessionCipher.decryptMsg(WhisperMessage(serialized=bobOutgoingMessage.serialize())) + $this->assertEquals(alicePlaintext, originalMessage) + + self.runInteraction(aliceStore, bobStore) + + aliceStore = InMemoryAxolotlStore() + aliceSessionBuilder = SessionBuilder(aliceStore, aliceStore, aliceStore, aliceStore, self::BOB_RECIPIENT_ID, 1) + aliceSessionCipher = SessionCipher(aliceStore, aliceStore, aliceStore, aliceStore, self::BOB_RECIPIENT_ID, 1) + + bobPreKeyPair = Curve::generateKeyPair() + bobSignedPreKeyPair = Curve::generateKeyPair() + bobSignedPreKeySignature = Curve::calculateSignature(bobStore.getIdentityKeyPair().getPrivateKey(), bobSignedPreKeyPair.getPublicKey().serialize()) + bobPreKey = PreKeyBundle(bobStore.getLocalRegistrationId(), + 1, 31338, bobPreKeyPair.getPublicKey(), + 23, bobSignedPreKeyPair.getPublicKey(), bobSignedPreKeySignature, + bobStore.getIdentityKeyPair().getPublicKey()) + + bobStore.storePreKey(31338, PreKeyRecord(bobPreKey.getPreKeyId(), bobPreKeyPair)) + bobStore.storeSignedPreKey(23, SignedPreKeyRecord(23, int(time.time() * 1000), bobSignedPreKeyPair, bobSignedPreKeySignature)) + aliceSessionBuilder.processPreKeyBundle(bobPreKey) + + outgoingMessage = aliceSessionCipher.encrypt(originalMessage) + + try: + plaintext = bobSessionCipher.decryptPkmsg(PreKeyWhisperMessage(serialized=outgoingMessage)) + throw new AssertionError("shouldn't be trusted!") + except Exception: + bobStore.saveIdentity(self::ALICE_RECIPIENT_ID, PreKeyWhisperMessage(serialized=outgoingMessage.serialize()).getIdentityKey()) + + plaintext = bobSessionCipher.decryptPkmsg(PreKeyWhisperMessage(serialized=outgoingMessage.serialize())) + $this->assertEquals(plaintext, originalMessage) + + + bobPreKey = PreKeyBundle(bobStore.getLocalRegistrationId(), 1, + 31337, Curve::generateKeyPair().getPublicKey(), + 23, bobSignedPreKeyPair.getPublicKey(), bobSignedPreKeySignature, + aliceStore.getIdentityKeyPair().getPublicKey()) + try: + aliceSessionBuilder.process(bobPreKey) + throw new AssertionError("shouldn't be trusted!") + except Exception: + #good + pass + + public function test_badSignedPreKeySignature(){ + aliceStore = InMemoryAxolotlStore() + aliceSessionBuilder = SessionBuilder(aliceStore, aliceStore, aliceStore, aliceStore, + self::BOB_RECIPIENT_ID, 1) + + bobIdentityKeyStore = InMemoryIdentityKeyStore() + + bobPreKeyPair = Curve::generateKeyPair() + bobSignedPreKeyPair = Curve::generateKeyPair() + bobSignedPreKeySignature = Curve::calculateSignature(bobIdentityKeyStore.getIdentityKeyPair().getPrivateKey(), + bobSignedPreKeyPair.getPublicKey().serialize()) + + for i in range(0, len(bobSignedPreKeySignature) * 8): + modifiedSignature = bytearray(bobSignedPreKeySignature[:]) + modifiedSignature[int(i/8)] ^= 0x01 << (i % 8) + + bobPreKey = PreKeyBundle(bobIdentityKeyStore.getLocalRegistrationId(), 1, + 31337, bobPreKeyPair.getPublicKey(), + 22, bobSignedPreKeyPair.getPublicKey(), modifiedSignature, + bobIdentityKeyStore.getIdentityKeyPair().getPublicKey()) + + try: + aliceSessionBuilder.processPreKeyBundle(bobPreKey) + except Exception: + pass + #good + bobPreKey = PreKeyBundle(bobIdentityKeyStore.getLocalRegistrationId(), 1, + 31337, bobPreKeyPair.getPublicKey(), + 22, bobSignedPreKeyPair.getPublicKey(), bobSignedPreKeySignature, + bobIdentityKeyStore.getIdentityKeyPair().getPublicKey()) + + aliceSessionBuilder.processPreKeyBundle(bobPreKey) + +*/ + + public function test_basicKeyExchange() + { + $aliceStore = new InMemoryAxolotlStore(); + $aliceSessionBuilder = new SessionBuilder($aliceStore, $aliceStore, $aliceStore, $aliceStore, self::BOB_RECIPIENT_ID, 1); + + $bobStore = new InMemoryAxolotlStore(); + $bobSessionBuilder = new SessionBuilder($bobStore, $bobStore, $bobStore, $bobStore, self::ALICE_RECIPIENT_ID, 1); + + $aliceKeyExchangeMessage = $aliceSessionBuilder->processInitKeyExchangeMessage(); + $this->assertTrue($aliceKeyExchangeMessage != null); + + $aliceKeyExchangeMessageBytes = $aliceKeyExchangeMessage->serialize(); + + $bobKeyExchangeMessage = $bobSessionBuilder->processKeyExchangeMessage( + new KeyExchangeMessage(null, null, null, null, null, null, null, $aliceKeyExchangeMessageBytes)); + + $this->assertTrue($bobKeyExchangeMessage != null); + + define('TEST', true); + $bobKeyExchangeMessageBytes = $bobKeyExchangeMessage->serialize(); + $response = $aliceSessionBuilder->processKeyExchangeMessage(new KeyExchangeMessage(null, null, null, null, null, null, null, $bobKeyExchangeMessageBytes)); + + $this->assertTrue($response == null); + $this->assertTrue($aliceStore->containsSession(self::BOB_RECIPIENT_ID, 1)); + $this->assertTrue($bobStore->containsSession(self::ALICE_RECIPIENT_ID, 1)); + + $this->runInteraction($aliceStore, $bobStore); + + $aliceStore = new InMemoryAxolotlStore(); + $aliceSessionBuilder = new SessionBuilder($aliceStore, $aliceStore, $aliceStore, $aliceStore, self::BOB_RECIPIENT_ID, 1); + $aliceKeyExchangeMessage = $aliceSessionBuilder->processInitKeyExchangeMessage(); + + try { + $bobKeyExchangeMessage = $bobSessionBuilder->processKeyExchangeMessage($aliceKeyExchangeMessage); + throw new AssertionError("This identity shouldn't be trusted!"); + } catch (UntrustedIdentityException $ex) { + $bobStore->saveIdentity(self::ALICE_RECIPIENT_ID, $aliceKeyExchangeMessage->getIdentityKey()); + } + $bobKeyExchangeMessage = $bobSessionBuilder->processKeyExchangeMessage($aliceKeyExchangeMessage); + + $this->assertTrue($aliceSessionBuilder->processKeyExchangeMessage($bobKeyExchangeMessage) == null); + + self.runInteraction($aliceStore, $bobStore); + } + + public function runInteraction($aliceStore, $bobStore) + { + /* + :type aliceStore: AxolotlStore + :type bobStore: AxolotlStore + */ + + $aliceSessionCipher = new SessionCipher($aliceStore, $aliceStore, $aliceStore, $aliceStore, self::BOB_RECIPIENT_ID, 1); + $bobSessionCipher = new SessionCipher($bobStore, $bobStore, $bobStore, $bobStore, self::ALICE_RECIPIENT_ID, 1); + + $originalMessage = 'smert ze smert'; + $aliceMessage = $aliceSessionCipher->encrypt($originalMessage); + + $this->assertTrue($aliceMessage->getType() == CiphertextMessage::WHISPER_TYPE); + $plaintext = $bobSessionCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $aliceMessage->serialize())); + $this->assertEquals($plaintext, $originalMessage); + + $bobMessage = $bobSessionCipher->encrypt($originalMessage); + + $this->assertTrue($bobMessage->getType() == CiphertextMessage::WHISPER_TYPE); + + $plaintext = $aliceSessionCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $bobMessage->serialize())); + $this->assertEquals($plaintext, $originalMessage); + + for ($i = 0; $i < 10; $i++) { + $loopingMessage = 'What do we mean by saying that existence precedes essence? '. + 'We mean that man first of all exists, encounters himself, '. + 'surges up in the world--and defines himself aftward. '.$i; + $aliceLoopingMessage = $aliceSessionCipher->encrypt($loopingMessage); + $loopingPlaintext = $bobSessionCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $aliceLoopingMessage->serialize())); + $this->assertEquals($loopingPlaintext, $loopingMessage); + } + + for ($i = 0; $i < 10; $i++) { + $loopingMessage = 'What do we mean by saying that existence precedes essence? '. + 'We mean that man first of all exists, encounters himself, '. + 'surges up in the world--and defines himself aftward. '.$i; + $bobLoopingMessage = $bobSessionCipher->encrypt($loopingMessage); + + $loopingPlaintext = $aliceSessionCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $bobLoopingMessage->serialize())); + $this->assertEquals($loopingPlaintext, $loopingMessage); + } + $aliceOutOfOrderMessages = []; + + for ($i = 0; $i < 10; $i++) { + $loopingMessage = 'What do we mean by saying that existence precedes essence? '. + 'We mean that man first of all exists, encounters himself, '. + 'surges up in the world--and defines himself aftward. '.$i; + $aliceLoopingMessage = $aliceSessionCipher->encrypt($loopingMessage); + $aliceOutOfOrderMessages[] = [$loopingMessage, $aliceLoopingMessage]; + } + for ($i = 0; $i < 10; $i++) { + $loopingMessage = 'What do we mean by saying that existence precedes essence? '. + 'We mean that man first of all exists, encounters himself, '. + 'surges up in the world--and defines himself aftward.'.$i; + $aliceLoopingMessage = $aliceSessionCipher->encrypt($loopingMessage); + $loopingPlaintext = $bobSessionCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $aliceLoopingMessage->serialize())); + $this->assertEquals($loopingPlaintext, $loopingMessage); + } + for ($i = 0; $i < 10; $i++) { + $loopingMessage = 'You can only desire based on what you know: '.$i; + $bobLoopingMessage = $bobSessionCipher->encrypt($loopingMessage); + + $loopingPlaintext = $aliceSessionCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $bobLoopingMessage->serialize())); + $this->assertEquals($loopingPlaintext, $loopingMessage); + } + foreach ($aliceOutOfOrderMessages as $aliceOutOfOrderMessage) { + $outOfOrderPlaintext = $bobSessionCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $aliceOutOfOrderMessage[1]->serialize())); + $this->assertEquals($outOfOrderPlaintext, $aliceOutOfOrderMessage[0]); + } + } +} diff --git a/src/libaxolotl-php/tests/test_sessioncipher.php b/src/libaxolotl-php/tests/test_sessioncipher.php new file mode 100755 index 00000000..cbe6f0d3 --- /dev/null +++ b/src/libaxolotl-php/tests/test_sessioncipher.php @@ -0,0 +1,272 @@ + 230) { + $txt = 'HEX:'.bin2hex($txt); + + return $txt; + } + } + + return $txt; +} +function niceVarDump($obj, $ident = 0) +{ + $data = ''; + $data .= str_repeat(' ', $ident); + $original_ident = $ident; + $toClose = false; + switch (gettype($obj)) { + case 'object': + $vars = (array) $obj; + $data .= gettype($obj).' ('.get_class($obj).') ('.count($vars).") {\n"; + $ident += 2; + foreach ($vars as $key => $var) { + $type = ''; + $k = bin2hex($key); + if (strpos($k, '002a00') === 0) { + $k = str_replace('002a00', '', $k); + $type = ':protected'; + } elseif (strpos($k, bin2hex("\x00".get_class($obj)."\x00")) === 0) { + $k = str_replace(bin2hex("\x00".get_class($obj)."\x00"), '', $k); + $type = ':private'; + } + $k = hex2bin($k); + if (is_subclass_of($obj, 'ProtobufMessage') && $k == 'values') { + $r = new ReflectionClass($obj); + $constants = $r->getConstants(); + $newVar = []; + foreach ($constants as $ckey => $cval) { + if (substr($ckey, 0, 3) != 'PB_') { + $newVar[$ckey] = $var[$cval]; + } + } + $var = $newVar; + } + $data .= str_repeat(' ', $ident)."[$k$type]=>\n".niceVarDump($var, $ident)."\n"; + } + $toClose = true; + break; + case 'array': + $data .= 'array ('.count($obj).") {\n"; + $ident += 2; + foreach ($obj as $key => $val) { + $data .= str_repeat(' ', $ident).'['.(is_int($key) ? $key : "\"$key\"")."]=>\n".niceVarDump($val, $ident)."\n"; + } + $toClose = true; + break; + case 'string': + $data .= 'string "'.parseText($obj)."\"\n"; + break; + case 'NULL': + $data .= gettype($obj); + break; + default: + $data .= gettype($obj).'('.strval($obj).")\n"; + break; + } + if ($toClose) { + $data .= str_repeat(' ', $original_ident)."}\n"; + } + + return $data; +} +class SessionCipherTest extends PHPUnit_Framework_TestCase +{ + public function test_basicSessionV2() + { + $aliceSessionRecord = new SessionRecord(); + $bobSessionRecord = new SessionRecord(); + $this->initializeSessionsV2($aliceSessionRecord->getSessionState(), $bobSessionRecord->getSessionState()); + $this->runInteraction($aliceSessionRecord, $bobSessionRecord); + } + + public function test_basicSessionV3() + { + $aliceSessionRecord = new SessionRecord(); + $bobSessionRecord = new SessionRecord(); + $this->initializeSessionsV3($aliceSessionRecord->getSessionState(), $bobSessionRecord->getSessionState()); + $this->runInteraction($aliceSessionRecord, $bobSessionRecord); + } + + protected function runInteraction($aliceSessionRecord, $bobSessionRecord) + { + $aliceStore = new InMemoryAxolotlStore(); + $bobStore = new InMemoryAxolotlStore(); + + $aliceStore->storeSession(2, 1, $aliceSessionRecord); + $bobStore->storeSession(3, 1, $bobSessionRecord); + + $aliceCipher = new SessionCipher($aliceStore, $aliceStore, $aliceStore, $aliceStore, 2, 1); + $bobCipher = new SessionCipher($bobStore, $bobStore, $bobStore, $bobStore, 3, 1); + + $alicePlaintext = 'This is a plaintext message.'; + + $message = $aliceCipher->encrypt($alicePlaintext); + $bobPlaintext = $bobCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $message->serialize())); + $this->assertEquals($alicePlaintext, $bobPlaintext); + + $bobReply = 'This is a message from Bob.'; + $reply = $bobCipher->encrypt($bobReply); + $receivedReply = $aliceCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $reply->serialize())); + + $this->assertEquals($bobReply, $receivedReply); + + $aliceCiphertextMessages = []; + $alicePlaintextMessages = []; + + for ($i = 0; $i < 50; $i++) { + $alicePlaintextMessages[] = 'смерть за смерть '.$i; + $aliceCiphertextMessages[] = $aliceCipher->encrypt("смерть за смерть $i"); + } + //shuffle(aliceCiphertextMessages) + //shuffle(alicePlaintextMessages) + + for ($i = 0; $i < count($aliceCiphertextMessages) / 2; $i++) { + $receivedPlaintext = $bobCipher->decryptMsg(new WhisperMessage(null, null, null, null, null, null, null, null, $aliceCiphertextMessages[$i]->serialize())); + $this->assertEquals($receivedPlaintext, $alicePlaintextMessages[$i]); + } + } + + /* """ + + List bobCiphertextMessages = new ArrayList<>(); + List bobPlaintextMessages = new ArrayList<>(); + + for (int i=0;i<20;i++) { + bobPlaintextMessages.add(("смерть за смерть " + i).getBytes()); + bobCiphertextMessages.add(bobCipher.encrypt(("смерть за смерть " + i).getBytes())); + } + + seed = System.currentTimeMillis(); + + Collections.shuffle(bobCiphertextMessages, new Random(seed)); + Collections.shuffle(bobPlaintextMessages, new Random(seed)); + + for (int i=0;igetPublicKey()), + $aliceIdentityKeyPair->getPrivateKey()); + $aliceBaseKey = Curve::generateKeyPair(); + $aliceEphemeralKey = Curve::generateKeyPair(); + + $bobIdentityKeyPair = Curve::generateKeyPair(); + $bobIdentityKey = new IdentityKeyPair(new IdentityKey($bobIdentityKeyPair->getPublicKey()), + $bobIdentityKeyPair->getPrivateKey()); + $bobBaseKey = Curve::generateKeyPair(); + $bobEphemeralKey = $bobBaseKey; + + $aliceParameters = AliceAxolotlParameters::newBuilder(); + + $aliceParameters->setOurIdentityKey($aliceIdentityKey) + ->setOurBaseKey($aliceBaseKey) + ->setTheirIdentityKey($bobIdentityKey->getPublicKey()) + ->setTheirSignedPreKey($bobEphemeralKey->getPublicKey()) + ->setTheirRatchetKey($bobEphemeralKey->getPublicKey()) + ->setTheirOneTimePreKey(null); + $aliceParameters = $aliceParameters->create(); + + $bobParameters = BobAxolotlParameters::newBuilder(); + $bobParameters->setOurIdentityKey($bobIdentityKey) + ->setOurOneTimePreKey(null) + ->setOurRatchetKey($bobEphemeralKey) + ->setOurSignedPreKey($bobBaseKey) + ->setTheirBaseKey($aliceBaseKey->getPublicKey()) + ->setTheirIdentityKey($aliceIdentityKey->getPublicKey()); + $bobParameters = $bobParameters->create(); + + RatchetingSession::initializeSessionAsAlice($aliceSessionState, 2, $aliceParameters); + RatchetingSession::initializeSessionAsBob($bobSessionState, 2, $bobParameters); + } + + protected function initializeSessionsV3($aliceSessionState, $bobSessionState) + { + $aliceIdentityKeyPair = Curve::generateKeyPair(); + $aliceIdentityKey = new IdentityKeyPair(new IdentityKey($aliceIdentityKeyPair->getPublicKey()), + $aliceIdentityKeyPair->getPrivateKey()); + $aliceBaseKey = Curve::generateKeyPair(); + $aliceEphemeralKey = Curve::generateKeyPair(); + + $alicePreKey = $aliceBaseKey; + + $bobIdentityKeyPair = Curve::generateKeyPair(); + $bobIdentityKey = new IdentityKeyPair(new IdentityKey($bobIdentityKeyPair->getPublicKey()), + $bobIdentityKeyPair->getPrivateKey()); + $bobBaseKey = Curve::generateKeyPair(); + $bobEphemeralKey = $bobBaseKey; + + $bobPreKey = Curve::generateKeyPair(); + + $aliceParameters = AliceAxolotlParameters::newBuilder() + ->setOurBaseKey($aliceBaseKey) + ->setOurIdentityKey($aliceIdentityKey) + ->setTheirOneTimePreKey(null) + ->setTheirRatchetKey($bobEphemeralKey->getPublicKey()) + ->setTheirSignedPreKey($bobBaseKey->getPublicKey()) + ->setTheirIdentityKey($bobIdentityKey->getPublicKey()) + ->create(); + + $bobParameters = BobAxolotlParameters::newBuilder() + ->setOurRatchetKey($bobEphemeralKey) + ->setOurSignedPreKey($bobBaseKey) + ->setOurOneTimePreKey(null) + ->setOurIdentityKey($bobIdentityKey) + ->setTheirIdentityKey($aliceIdentityKey->getPublicKey()) + ->setTheirBaseKey($aliceBaseKey->getPublicKey()) + ->create(); + + RatchetingSession::initializeSessionAsAlice($aliceSessionState, 3, $aliceParameters); + RatchetingSession::initializeSessionAsBob($bobSessionState, 3, $bobParameters); + } +} diff --git a/src/libaxolotl-php/tests/test_sigs.php b/src/libaxolotl-php/tests/test_sigs.php new file mode 100755 index 00000000..881ae4e3 --- /dev/null +++ b/src/libaxolotl-php/tests/test_sigs.php @@ -0,0 +1,67 @@ +assertEquals($sharedOne, $shared); + $this->assertEquals($sharedTwo, $shared); + } + + public function test_randomAgreements() + { + for ($i = 0; $i < 50; $i++) { + $alice = Curve::generateKeyPair(); + $bob = Curve::generateKeyPair(); + $sharedAlice = Curve::calculateAgreement($bob->getPublicKey(), $alice->getPrivateKey()); + $sharedBob = Curve::calculateAgreement($alice->getPublicKey(), $bob->getPrivateKey()); + $this->assertEquals($sharedAlice, $sharedBob); + } + } + + public function test_gensig() + { + $identityKeyPair = KeyHelper::generateIdentityKeyPair(); + KeyHelper::generateSignedPreKey($identityKeyPair, 0); + } + + public function test_signature() + { + $aliceIdentityPrivate = "\xc0\x97\x24\x84\x12\xe5\x8b\xf0\x5d\xf4\x87\x96\x82\x05\x13\x27\x94\x17\x8e\x36\x76\x37\xf5\x81\x8f\x81\xe0\xe6\xce\x73\xe8\x65"; + + $aliceIdentityPublic = "\x05\xab\x7e\x71\x7d\x4a\x16\x3b\x7d\x9a\x1d\x80\x71\xdf\xe9\xdc\xf8\xcd\xcd\x1c\xea\x33\x39\xb6\x35\x6b\xe8\x4d\x88\x7e\x32\x2c\x64"; + + $aliceEphemeralPublic = "\x05\xed\xce\x9d\x9c\x41\x5c\xa7\x8c\xb7\x25\x2e\x72\xc2\xc4\xa5\x54\xd3\xeb\x29\x48\x5a\x0e\x1d\x50\x31\x18\xd1\xa8\x2d\x99\xfb\x4a"; + + $aliceSignature = "\x5d\xe8\x8c\xa9\xa8\x9b\x4a\x11\x5d\xa7\x91\x09\xc6\x7c\x9c\x74\x64\xa3\xe4\x18\x02\x74\xf1\xcb\x8c\x63\xc2\x98\x4e\x28\x6d\xfb\xed\xe8\x2d\xeb\x9d\xcd\x9f\xae\x0b\xfb\xb8\x21\x56\x9b\x3d\x90\x01\xbd\x81\x30\xcd\x11\xd4\x86\xce\xf0\x47\xbd\x60\xb8\x6e\x88"; + + $alicePrivateKey = Curve::decodePrivatePoint($aliceIdentityPrivate); + $alicePublicKey = Curve::decodePoint($aliceIdentityPublic, 0); + $aliceEphemeral = Curve::decodePoint($aliceEphemeralPublic, 0); + + $res = Curve::verifySignature($alicePublicKey, $aliceEphemeral->serialize(), $aliceSignature); + $this->assertTrue($res); + } +} diff --git a/src/libaxolotl-php/tests/util/test_byteutil.php b/src/libaxolotl-php/tests/util/test_byteutil.php new file mode 100755 index 00000000..e640f826 --- /dev/null +++ b/src/libaxolotl-php/tests/util/test_byteutil.php @@ -0,0 +1,34 @@ +assertEquals($result[0], $a_data); + $this->assertEquals($result[1], $b_data); + $this->assertEquals($result[2], $c_data); + } +} diff --git a/src/libaxolotl-php/util/ByteUtil.php b/src/libaxolotl-php/util/ByteUtil.php new file mode 100755 index 00000000..6d5ef976 --- /dev/null +++ b/src/libaxolotl-php/util/ByteUtil.php @@ -0,0 +1,231 @@ +> 4; + } + + public static function lowBitsToInt($value) // [byte value] + { + if (is_string($value)) { + $value = ord($value[0]); + } + + return $value & 0xF; + } + + public static function highBitsToMedium($value) // [int value] + { + if (is_string($value)) { + $value = ord($value[0]); + } + + return $value >> 12; + } + + public static function lowBitsToMedium($value) // [int value] + { + return $value & 0xFFF; + } + + public static function shortToByteArray_197ef($value) // [int value] + { + $bytes = []; + self::shortToByteArray($bytes, 0, $value); + + return $bytes; + } + + public static function shortToByteArray_21c8b6ca($bytes, $offset, $value) // [byte[] bytes, int offset, int value] + { + $bytes[($offset + 1)] = $value; + $bytes[$offset] = (($value >> 8)); + + return 2; + } + + public static function shortToLittleEndianByteArray($bytes, $offset, $value) // [byte[] bytes, int offset, int value] + { + $bytes[$offset] = $value; + $bytes[($offset + 1)] = (($value >> 8)); + + return 2; + } + + public static function mediumToByteArray_197ef($value) // [int value] + { + $bytes = []; + self::mediumToByteArray($bytes, 0, $value); + + return $bytes; + } + + public static function mediumToByteArray_21c8b6ca($bytes, $offset, $value) // [byte[] bytes, int offset, int value] + { + $bytes[($offset + 2)] = $value; + $bytes[($offset + 1)] = (($value >> 8)); + $bytes[$offset] = (($value >> 16)); + + return 3; + } + + public static function intToByteArray_197ef($value) // [int value] + { + $bytes = []; + self::intToByteArray_21c8b6ca($bytes, 0, $value); + + return $bytes; + } + + public static function intToByteArray_21c8b6ca(&$bytes, $offset, $value) // [byte[] bytes, int offset, int value] + { + $bytes = unpack('C*', pack('L', $value)); + //$bytes[$offset + 3] = $value; + //$bytes[$offset + 2] = (($value >> 8)); + //$bytes[$offset + 1] = (($value >> 16)); + //$bytes[$offset] = (($value >> 24)); + return 4; + } + + public static function intToLittleEndianByteArray($bytes, $offset, $value) // [byte[] bytes, int offset, int value] + { + $bytes[$offset] = $value; + $bytes[($offset + 1)] = (($value >> 8)); + $bytes[($offset + 2)] = (($value >> 16)); + $bytes[($offset + 3)] = (($value >> 24)); + + return 4; + } + + public static function longToByteArray_32c67c($l) // [long l] + { + $bytes = []; + self::longToByteArray($bytes, 0, $l); + + return $bytes; + } + + public static function longToByteArray_174f8301($bytes, $offset, $value) // [byte[] bytes, int offset, long value] + { + $bytes[($offset + 7)] = $value; + $bytes[($offset + 6)] = (($value >> 8)); + $bytes[($offset + 5)] = (($value >> 16)); + $bytes[($offset + 4)] = (($value >> 24)); + $bytes[($offset + 3)] = (($value >> 32)); + $bytes[($offset + 2)] = (($value >> 40)); + $bytes[($offset + 1)] = (($value >> 48)); + $bytes[$offset] = (($value >> 56)); + + return 8; + } + + public static function longTo4ByteArray($bytes, $offset, $value) // [byte[] bytes, int offset, long value] + { + $bytes[($offset + 3)] = $value; + $bytes[($offset + 2)] = (($value >> 8)); + $bytes[($offset + 1)] = (($value >> 16)); + $bytes[($offset + 0)] = (($value >> 24)); + + return 4; + } + + public static function byteArrayToShort_ae1a4a6a($bytes) // [byte[] bytes] + { + return self::byteArrayToShort($bytes, 0); + } + + public static function byteArrayToShort_29e7cc9a($bytes, $offset) // [byte[] bytes, int offset] + { + return ((($bytes[$offset] & 0xff)) << 8) | (($bytes[($offset + 1)] & 0xff)); + } + + public static function byteArrayToMedium($bytes, $offset) // [byte[] bytes, int offset] + { + return (((($bytes[$offset] & 0xff)) << 16) | ((($bytes[($offset + 1)] & 0xff)) << 8)) | (($bytes[($offset + 2)] & 0xff)); + } + + public static function byteArrayToInt_ae1a4a6a($bytes) // [byte[] bytes] + { + return self::byteArrayToInt($bytes, 0); + } + + public static function byteArrayToInt_29e7cc9a($bytes, $offset) // [byte[] bytes, int offset] + { + return ((((($bytes[$offset] & 0xff)) << 24) | ((($bytes[($offset + 1)] & 0xff)) << 16)) | ((($bytes[($offset + 2)] & 0xff)) << 8)) | (($bytes[($offset + 3)] & 0xff)); + } + + public static function byteArrayToIntLittleEndian($bytes, $offset) // [byte[] bytes, int offset] + { + return ((((($bytes[($offset + 3)] & 0xff)) << 24) | ((($bytes[($offset + 2)] & 0xff)) << 16)) | ((($bytes[($offset + 1)] & 0xff)) << 8)) | (($bytes[$offset] & 0xff)); + } + + public static function byteArrayToLong_ae1a4a6a($bytes) // [byte[] bytes] + { + return self::byteArrayToLong($bytes, 0); + } + + public static function byteArray4ToLong($bytes, $offset) // [byte[] bytes, int offset] + { + return (((((($bytes[($offset + 0)] & 0xff)) << 24)) | (((($bytes[($offset + 1)] & 0xff)) << 16))) | (((($bytes[($offset + 2)] & 0xff)) << 8))) | ((($bytes[($offset + 3)] & 0xff))); + } + + public static function byteArrayToLong_29e7cc9a($bytes, $offset) // [byte[] bytes, int offset] + { + return (((((((((($bytes[$offset] & 0xff)) << 56)) | (((($bytes[($offset + 1)] & 0xff)) << 48))) | (((($bytes[($offset + 2)] & 0xff)) << 40))) | (((($bytes[($offset + 3)] & 0xff)) << 32))) | (((($bytes[($offset + 4)] & 0xff)) << 24))) | (((($bytes[($offset + 5)] & 0xff)) << 16))) | (((($bytes[($offset + 6)] & 0xff)) << 8))) | ((($bytes[($offset + 7)] & 0xff))); + } +} diff --git a/src/libaxolotl-php/util/Helper.php b/src/libaxolotl-php/util/Helper.php new file mode 100755 index 00000000..37f7fd04 --- /dev/null +++ b/src/libaxolotl-php/util/Helper.php @@ -0,0 +1,17 @@ +getPublicKey()); + $serialized = '0a21056e8936e8367f768a7bba008ade7cf58407bdc7a6aae293e2cb7c06668dcd7d5e12205011524f0c15467100dd6'. + '03e0d6020f4d293edfbcd82129b14a88791ac81365c'; + $serialized = pack('H*', $serialized); + $identityKeyPair = new IdentityKeyPair($publicKey, $keyPair->getPrivateKey()); + + return $identityKeyPair; + // return new IdentityKeyPair(serialized=serialized) + } + + /* + Generate a registration ID. Clients should only do this once, + at install time. + */ + + public static function generateRegistrationId() + { + $regId = self::getRandomSequence(); + + return $regId; + } + + public static function getRandomSequence($max = 4294967296) + { + $size = (int) ((log($max) / log(2)) / 8); + $rand = openssl_random_pseudo_bytes((int) $size); + $randh = unpack('H*', $rand); + + return intval($randh[1], 16); + } + + /* + Generate a list of PreKeys. Clients should do this at install time, and + subsequently any time the list of PreKeys stored on the server runs low. + PreKey IDs are shorts, so they will eventually be repeated. Clients should + store PreKeys in a circular buffer, so that they are repeated as infrequently + as possible. + @param start The starting PreKey ID, inclusive. + @param count The number of PreKeys to generate. + @return the list of generated PreKeyRecords. + */ + + public static function generatePreKeys($start, $count) + { + $results = []; + $start -= 1; + for ($i = 0; $i < $count; $i++) { + $preKeyId = (($start + $i) % (Medium::MAX_VALUE - 1)) + 1; + $results[] = (new PreKeyRecord($preKeyId, Curve::generateKeyPair())); + } + + return $results; + } + + public static function generateSignedPreKey($identityKeyPair, $signedPreKeyId) + { + $keyPair = Curve::generateKeyPair(); + $signature = Curve::calculateSignature($identityKeyPair->getPrivateKey(), $keyPair->getPublicKey()->serialize()); + + $spk = new SignedPreKeyRecord($signedPreKeyId, (int) round(time() * 1000), $keyPair, $signature); + + return $spk; + } + + public static function generateSenderSigningKey() + { + return Curve::generateKeyPair(); + } + + public static function generateSenderKey() + { + return openssl_random_pseudo_bytes(32); + } + + public static function generateSenderKeyId() + { + return self::getRandomSequence(2147483647); + } +} diff --git a/src/libaxolotl-php/util/Medium.php b/src/libaxolotl-php/util/Medium.php new file mode 100755 index 00000000..b1ee1247 --- /dev/null +++ b/src/libaxolotl-php/util/Medium.php @@ -0,0 +1,6 @@ +__init(); + $me->v1 = $v1; + $me->v2 = $v2; + + return $me; + } + + public function first() + { + return $this->v1; + } + + public function second() + { + return $this->v2; + } + + public function equals($o) // [Object o] + { + return ($o instanceof self && $this->equal($o::first(), $this->first())) && $this->equal($o::second(), $this->second()); + } + + public function hashCode() + { + return $this->first()->hashCode() ^ $this->second()->hashCode(); + } + + protected function equal($first, $second) // [Object first, Object second] + { + if ((($first == null) && ($second == null))) { + return true; + } + if ((($first == null) || ($second == null))) { + return false; + } + + return $first->equals($second); + } +} +Pair::__staticinit(); // initialize static vars for this class on load diff --git a/src/mediauploader.php b/src/mediauploader.php index 36b20ab7..c149d3f5 100755 --- a/src/mediauploader.php +++ b/src/mediauploader.php @@ -1,13 +1,12 @@ getChild("media")->getAttribute("url"); - $filepath = $messageContainer["filePath"]; - $to = $messageContainer["to"]; + $url = $uploadResponseNode->getChild('media')->getAttribute('url'); + $filepath = $messageContainer['filePath']; + $to = $messageContainer['to']; + return self::getPostString($filepath, $url, $mediafile, $to, $selfJID); } @@ -57,39 +55,33 @@ protected static function getPostString($filepath, $url, $mediafile, $to, $from) $host = parse_url($url, PHP_URL_HOST); //filename to md5 digest - $cryptoname = md5($filepath) . "." . $mediafile['fileextension']; - $boundary = "zzXXzzYYzzXXzzQQ"; - $contentlength = 0; + $cryptoname = md5($filepath).'.'.$mediafile['fileextension']; + $boundary = 'zzXXzzYYzzXXzzQQ'; - if(is_array($to)) { + if (is_array($to)) { $to = implode(',', $to); } - $hBAOS = "--" . $boundary . "\r\n"; + $hBAOS = '--'.$boundary."\r\n"; $hBAOS .= "Content-Disposition: form-data; name=\"to\"\r\n\r\n"; - $hBAOS .= $to . "\r\n"; - $hBAOS .= "--" . $boundary . "\r\n"; + $hBAOS .= $to."\r\n"; + $hBAOS .= '--'.$boundary."\r\n"; $hBAOS .= "Content-Disposition: form-data; name=\"from\"\r\n\r\n"; - $hBAOS .= $from . "\r\n"; - $hBAOS .= "--" . $boundary . "\r\n"; - $hBAOS .= "Content-Disposition: form-data; name=\"file\"; filename=\"" . $cryptoname . "\"\r\n"; - $hBAOS .= "Content-Type: " . $mediafile['filemimetype'] . "\r\n\r\n"; + $hBAOS .= $from."\r\n"; + $hBAOS .= '--'.$boundary."\r\n"; + $hBAOS .= 'Content-Disposition: form-data; name="file"; filename="'.$cryptoname."\"\r\n"; + $hBAOS .= 'Content-Type: '.$mediafile['filemimetype']."\r\n\r\n"; - $fBAOS = "\r\n--" . $boundary . "--\r\n"; + $fBAOS = "\r\n--".$boundary."--\r\n"; - $contentlength += strlen($hBAOS); - $contentlength += strlen($fBAOS); - $contentlength += $mediafile['filesize']; + $contentlength = strlen($hBAOS) + strlen($fBAOS) + $mediafile['filesize']; - $POST = "POST " . $url . "\r\n"; - $POST .= "Content-Type: multipart/form-data; boundary=" . $boundary . "\r\n"; - $POST .= "Host: " . $host . "\r\n"; - $POST .= "User-Agent: " . WhatsProt::WHATSAPP_USER_AGENT . "\r\n"; - $POST .= "Content-Length: " . $contentlength . "\r\n\r\n"; + $POST = 'POST '.$url."\r\n"; + $POST .= 'Content-Type: multipart/form-data; boundary='.$boundary."\r\n"; + $POST .= 'Host: '.$host."\r\n"; + $POST .= 'User-Agent: '.Constants::WHATSAPP_USER_AGENT."\r\n"; + $POST .= 'Content-Length: '.$contentlength."\r\n\r\n"; return self::sendData($host, $POST, $hBAOS, $filepath, $mediafile, $fBAOS); } - } - -?> diff --git a/src/networkinfo.csv b/src/networkinfo.csv new file mode 100644 index 00000000..75072977 --- /dev/null +++ b/src/networkinfo.csv @@ -0,0 +1,1703 @@ +289,649,088,2191,ab,Abkhazia,7,A-Mobile +289,649,068,1679,ab,Abkhazia,7,A-Mobile2 +289,649,067,1663,ab,Abkhazia,7,Aquafon +412,1042,088,2191,af,Afghanistan,93,Afghan Telecom Corp. (AT) +412,1042,080,2063,af,Afghanistan,93,Afghan Telecom Corp. (AT)2 +412,1042,001,31,af,Afghanistan,93,Afghan Wireless/AWCC +412,1042,040,1039,af,Afghanistan,93,Areeba +412,1042,050,1295,af,Afghanistan,93,Etisalat +412,1042,020,527,af,Afghanistan,93,Roshan +276,630,001,31,al,Albania,355,AMC Mobil +276,630,003,63,al,Albania,355,Eagle Mobile +276,630,004,79,al,Albania,355,PLUS Communication Sh.a +276,630,002,47,al,Albania,355,Vodafone +603,1539,001,31,dz,Algeria,213,ATM Mobils +603,1539,002,47,dz,Algeria,213,Orascom / DJEZZY +603,1539,003,63,dz,Algeria,213,Wataniya / Nedjma +544,1348,011,287,as,American Samoa,684,Blue Sky Communications +213,531,003,63,ad,Andorra,376,Mobiland +631,1585,004,79,ao,Angola,244,MoviCel +631,1585,002,47,ao,Angola,244,Unitel +365,869,840,2112,ai,Anguilla,1264,Cable and Wireless +365,869,010,16,ai,Anguilla,1264,Digicell / Wireless Vent. Ltd +344,836,030,48,ag,Antigua and Barbuda,1268,APUA PCS +344,836,920,2336,ag,Antigua and Barbuda,1268,C & W +344,836,930,2352,ag,Antigua and Barbuda,1268,Cing. Wirel./DigiCel +722,1826,310,784,ar,Argentina Republic,54,Claro/ CTI/AMX +722,1826,330,816,ar,Argentina Republic,54,Claro/ CTI/AMX2 +722,1826,320,800,ar,Argentina Republic,54,Claro/ CTI/AMX3 +722,1826,010,16,ar,Argentina Republic,54,Compania De Radiocomunicaciones Moviles SA +722,1826,070,112,ar,Argentina Republic,54,Movistar/Telefonica +722,1826,020,32,ar,Argentina Republic,54,Nextel +722,1826,341,833,ar,Argentina Republic,54,Telecom Personal S.A. +283,643,001,31,am,Armenia,374,ArmenTel/Beeline +283,643,004,4,am,Armenia,374,Karabakh Telecom +283,643,010,271,am,Armenia,374,Orange +283,643,005,95,am,Armenia,374,Vivacell +363,867,020,527,aw,Aruba,297,Digicel +363,867,001,31,aw,Aruba,297,Setar GSM +505,1285,014,335,au,Australia,61,AAPT Ltd. +505,1285,024,591,au,Australia,61,Advanced Comm Tech Pty. +505,1285,009,159,au,Australia,61,Airnet Commercial Australia Ltd. +505,1285,004,79,au,Australia,61,Department of Defense +505,1285,026,623,au,Australia,61,Dialogue Communications Pty Ltd +505,1285,012,303,au,Australia,61,H3G Ltd. +505,1285,006,111,au,Australia,61,H3G Ltd. +505,1285,088,2191,au,Australia,61,Localstar Holding Pty. Ltd +505,1285,019,415,au,Australia,61,Lycamobile Pty Ltd +505,1285,008,143,au,Australia,61,Railcorp/Vodafone +505,1285,099,2463,au,Australia,61,Railcorp/Vodafone2 +505,1285,013,319,au,Australia,61,Railcorp/Vodafone3 +505,1285,090,2319,au,Australia,61,Singtel Optus +505,1285,002,47,au,Australia,61,Singtel Optus2 +505,1285,001,31,au,Australia,61,Telstra Corp. Ltd. +505,1285,011,287,au,Australia,61,Telstra Corp. Ltd.2 +505,1285,071,1823,au,Australia,61,Telstra Corp. Ltd.3 +505,1285,072,1839,au,Australia,61,Telstra Corp. Ltd.4 +505,1285,005,95,au,Australia,61,The Ozitel Network Pty. +505,1285,016,367,au,Australia,61,Victorian Rail Track Corp. (VicTrack) +505,1285,007,127,au,Australia,61,Vodafone +505,1285,003,63,au,Australia,61,Vodafone2 +232,562,002,47,at,Austria,43,A1 MobilKom +232,562,011,287,at,Austria,43,A1 MobilKom2 +232,562,009,159,at,Austria,43,A1 MobilKom3 +232,562,001,31,at,Austria,43,A1 MobilKom4 +232,562,015,351,at,Austria,43,T-Mobile/Telering +232,562,000,15,at,Austria,43,Fix Line +232,562,010,271,at,Austria,43,H3G +232,562,014,335,at,Austria,43,H3G2 +232,562,012,303,at,Austria,43,Orange/One Connect +232,562,006,111,at,Austria,43,Orange/One Connect2 +232,562,005,95,at,Austria,43,Orange/One Connect +232,562,004,79,at,Austria,43,T-Mobile/Telering +232,562,003,63,at,Austria,43,T-Mobile/Telering2 +232,562,007,127,at,Austria,43,T-Mobile/Telering3 +232,562,008,143,at,Austria,43,Telefonica +400,1024,001,31,az,Azerbaijan,994,Azercell Telekom B.M. +400,1024,004,79,az,Azerbaijan,994,Azerfon. +400,1024,003,63,az,Azerbaijan,994,Caspian American Telecommunications LLC (CATEL) +400,1024,002,47,az,Azerbaijan,994,J.V. Bakcell GSM 2000 +364,868,030,783,bs,Bahamas,1242,Bahamas Telco. Comp. +364,868,390,912,bs,Bahamas,1242,Bahamas Telco. Comp.2 +364,868,039,927,bs,Bahamas,1242,Bahamas Telco. Comp.3 +364,868,003,3,bs,Bahamas,1242,Smart Communications +426,1062,001,31,bh,Bahrain,973,Batelco +426,1062,002,47,bh,Bahrain,973,MTC Vodafone +426,1062,004,79,bh,Bahrain,973,VIVA +470,1136,002,47,bd,Bangladesh,880,Robi/Aktel +470,1136,005,95,bd,Bangladesh,880,Citycell +470,1136,006,111,bd,Bangladesh,880,Citycell2 +470,1136,001,31,bd,Bangladesh,880,GrameenPhone +470,1136,003,63,bd,Bangladesh,880,Orascom +470,1136,004,79,bd,Bangladesh,880,TeleTalk +470,1136,007,127,bd,Bangladesh,880,Airtel/Warid +342,834,600,1536,bb,Barbados,1246,C & W BET Ltd. +342,834,810,2064,bb,Barbados,1246,Cingular Wireless +342,834,750,1872,bb,Barbados,1246,Digicel +342,834,050,80,bb,Barbados,1246,Digicel2 +342,834,820,2080,bb,Barbados,1246,Sunbeach +257,599,003,63,by,Belarus,375,BelCel JV +257,599,004,79,by,Belarus,375,BeST +257,599,001,31,by,Belarus,375,Mobile Digital Communications +257,599,002,47,by,Belarus,375,MTS +206,518,020,527,be,Belgium,32,Base/KPN +206,518,001,31,be,Belgium,32,Belgacom/Proximus +206,518,010,271,be,Belgium,32,Mobistar/Orange +206,518,002,47,be,Belgium,32,SNCT/NMBS +206,518,005,95,be,Belgium,32,Telenet BidCo NV +702,1794,067,1663,bz,Belize,501,DigiCell +702,1794,068,1679,bz,Belize,501,International Telco (INTELCO) +616,1558,004,79,bj,Benin,229,Bell Benin/BBCOM +616,1558,002,47,bj,Benin,229,Etisalat/MOOV +616,1558,005,5,bj,Benin,229,GloMobile +616,1558,001,31,bj,Benin,229,Libercom +616,1558,003,63,bj,Benin,229,MTN/Spacetel +350,848,000,0,bm,Bermuda,1441,Bermuda Digital Communications Ltd (BDC) +350,848,099,2463,bm,Bermuda,1441,CellOne Ltd +350,848,010,271,bm,Bermuda,1441,DigiCel / Cingular +350,848,002,47,bm,Bermuda,1441,M3 Wireless Ltd +350,848,001,31,bm,Bermuda,1441,Telecommunications (Bermuda & West Indies) Ltd (Digicel Bermuda) +402,1026,011,287,bt,Bhutan,975,B-Mobile +402,1026,017,383,bt,Bhutan,975,Bhutan Telecom Ltd (BTL) +402,1026,077,1919,bt,Bhutan,975,TashiCell +736,1846,002,47,bo,Bolivia,591,Entel Pcs +736,1846,001,31,bo,Bolivia,591,Nuevatel +736,1846,003,63,bo,Bolivia,591,TELECEL BOLIVIA +362,866,091,2335,bq,Bonaire Sint Eustatius and Saba,,United Telecommunications Services NV (UTS) +218,536,090,2319,ba,Bosnia & Herzegov.,387,BH Mobile +218,536,003,63,ba,Bosnia & Herzegov.,387,Eronet Mobile +218,536,005,95,ba,Bosnia & Herzegov.,387,M-Tel +652,1618,004,79,bw,Botswana,267,beMOBILE +652,1618,001,31,bw,Botswana,267,Mascom Wireless (Pty) Ltd. +652,1618,002,47,bw,Botswana,267,Orange +724,1828,012,303,br,Brazil,55,Claro/Albra/America Movil +724,1828,038,911,br,Brazil,55,Claro/Albra/America Movil2 +724,1828,005,95,br,Brazil,55,Claro/Albra/America Movil3 +724,1828,001,1,br,Brazil,55,Vivo S.A./Telemig +724,1828,033,831,br,Brazil,55,CTBC Celular SA (CTBC) +724,1828,032,815,br,Brazil,55,CTBC Celular SA (CTBC)2 +724,1828,034,847,br,Brazil,55,CTBC Celular SA (CTBC)3 +724,1828,008,8,br,Brazil,55,TIM +724,1828,000,15,br,Brazil,55,Nextel (Telet) +724,1828,039,927,br,Brazil,55,Nextel (Telet)2 +724,1828,030,783,br,Brazil,55,Oi (TNL PCS / Oi) +724,1828,031,799,br,Brazil,55,Oi (TNL PCS / Oi)2 +724,1828,024,591,br,Brazil,55,Amazonia Celular S/A +724,1828,016,367,br,Brazil,55,Brazil Telcom +724,1828,015,351,br,Brazil,55,Sercontel Cel +724,1828,07,7,br,Brazil,55,CTBC/Triangulo +724,1828,019,415,br,Brazil,55,Vivo S.A./Telemig +724,1828,003,63,br,Brazil,55,TIM +724,1828,002,47,br,Brazil,55,TIM2 +724,1828,004,79,br,Brazil,55,TIM3 +724,1828,037,895,br,Brazil,55,Unicel do Brasil Telecomunicacoes Ltda +724,1828,006,111,br,Brazil,55,Vivo S.A./Telemig +724,1828,023,575,br,Brazil,55,Vivo S.A./Telemig2 +724,1828,011,287,br,Brazil,55,Vivo S.A./Telemig3 +724,1828,010,271,br,Brazil,55,Vivo S.A./Telemig4 +348,840,570,1392,vg,British Virgin Islands,284,Caribbean Cellular +348,840,770,1904,vg,British Virgin Islands,284,Digicel +348,840,170,368,vg,British Virgin Islands,284,LIME +528,1320,002,47,bn,Brunei Darussalam,673,b-mobile +528,1320,011,287,bn,Brunei Darussalam,673,Datastream (DTSCom) +528,1320,001,31,bn,Brunei Darussalam,673,Telekom Brunei Bhd (TelBru) +284,644,006,111,bg,Bulgaria,359,BTC Mobile EOOD (vivatel) +284,644,003,63,bg,Bulgaria,359,BTC Mobile EOOD (vivatel)2 +284,644,005,95,bg,Bulgaria,359,Cosmo Mobile EAD/Globul +284,644,001,31,bg,Bulgaria,359,MobilTel AD +613,1555,003,63,bf,Burkina Faso,226,TeleCel +613,1555,001,31,bf,Burkina Faso,226,TeleMob-OnaTel +613,1555,002,47,bf,Burkina Faso,226,AirTel/ZAIN/CelTel +414,1044,001,31,mm,Burma/Myanmar,95,Myanmar Post & Teleco. +642,1602,002,47,bi,Burundi,257,Africel / Safaris +642,1602,008,143,bi,Burundi,257,HiTs Telecom +642,1602,003,63,bi,Burundi,257,Onatel / Telecel +642,1602,007,127,bi,Burundi,257,Smart Mobile / LACELL +642,1602,001,31,bi,Burundi,257,Spacetel / Econet +642,1602,082,2095,bi,Burundi,257,U-COM +456,1110,004,79,kh,Cambodia,855,Cambodia Advance Communications Co. Ltd (CADCOMMS) +456,1110,002,47,kh,Cambodia,855,Hello/Malaysia Telcom +456,1110,008,143,kh,Cambodia,855,Metfone +456,1110,018,399,kh,Cambodia,855,MFone/Camshin +456,1110,001,31,kh,Cambodia,855,Mobitel/Cam GSM +456,1110,003,63,kh,Cambodia,855,QB/Cambodia Adv. Comms. +456,1110,005,95,kh,Cambodia,855,Smart Mobile +456,1110,006,111,kh,Cambodia,855,Smart Mobile2 +456,1110,009,159,kh,Cambodia,855,Sotelco Ltd (Beeline Cambodia) +624,1572,001,31,cm,Cameroon,237,MTN +624,1572,002,47,cm,Cameroon,237,Orange +302,770,652,1618,ca,Canada,1,BC Tel Mobility +302,770,630,1584,ca,Canada,1,Bell Aliant +302,770,651,1617,ca,Canada,1,Bell Mobility +302,770,610,1552,ca,Canada,1,Bell Mobility2 +302,770,670,1648,ca,Canada,1,CityWest Mobility +302,770,360,864,ca,Canada,1,Clearnet +302,770,361,865,ca,Canada,1,Clearnet2 +302,770,380,896,ca,Canada,1,DMTS Mobility +302,770,710,1808,ca,Canada,1,Globalstar Canada +302,770,640,1600,ca,Canada,1,Latitude Wireless +302,770,370,880,ca,Canada,1,FIDO (Rogers AT&T/ Microcell) +302,770,320,800,ca,Canada,1,mobilicity +302,770,702,1794,ca,Canada,1,MT&T Mobility +302,770,655,1621,ca,Canada,1,MTS Mobility +302,770,660,1632,ca,Canada,1,MTS Mobility2 +302,770,701,1793,ca,Canada,1,NB Tel Mobility +302,770,703,1795,ca,Canada,1,New Tel Mobility +302,770,760,1888,ca,Canada,1,Public Mobile +302,770,657,1623,ca,Canada,1,Quebectel Mobility +302,770,720,1824,ca,Canada,1,Rogers AT&T Wireless +302,770,654,1620,ca,Canada,1,Sask Tel Mobility +302,770,680,1664,ca,Canada,1,Sask Tel Mobility2 +302,770,656,1622,ca,Canada,1,Tbay Mobility +302,770,220,544,ca,Canada,1,Telus Mobility +302,770,653,1619,ca,Canada,1,Telus Mobility2 +302,770,500,1280,ca,Canada,1,Videotron +302,770,490,1168,ca,Canada,1,WIND +625,1573,001,31,cv,Cape Verde,238,CV Movel +625,1573,002,47,cv,Cape Verde,238,T+ Telecom +346,838,050,80,ky,Cayman Islands,1345,Digicel Cayman Ltd +346,838,006,6,ky,Cayman Islands,1345,Digicel Ltd. +346,838,140,320,ky,Cayman Islands,1345,LIME / Cable & Wirel. +623,1571,001,31,cf,Central African Rep.,236,Centrafr. Telecom+ +623,1571,004,79,cf,Central African Rep.,236,Nationlink +623,1571,003,63,cf,Central African Rep.,236,Orange/Celca +623,1571,002,47,cf,Central African Rep.,236,Telecel Centraf. +622,1570,004,79,td,Chad,235,Salam/Sotel +622,1570,002,47,td,Chad,235,Tchad Mobile +622,1570,003,63,td,Chad,235,Tigo/Milicom/Tchad Mobile +622,1570,001,31,td,Chad,235,Zain/Airtel/Celtel +730,1840,006,111,cl,Chile,56,Blue Two Chile SA +730,1840,011,287,cl,Chile,56,Celupago SA +730,1840,015,351,cl,Chile,56,Cibeles Telecom SA +730,1840,003,63,cl,Chile,56,Claro +730,1840,010,271,cl,Chile,56,Entel PCS +730,1840,001,1,cl,Chile,56,Entel Telefonia Mov +730,1840,014,335,cl,Chile,56,Netline Telefonica Movil Ltda +730,1840,009,159,cl,Chile,56,Nextel SA +730,1840,005,95,cl,Chile,56,Nextel SA2 +730,1840,004,79,cl,Chile,56,Nextel SA3 +730,1840,002,47,cl,Chile,56,TELEFONICA +730,1840,007,127,cl,Chile,56,TELEFONICA2 +730,1840,012,303,cl,Chile,56,Telestar Movil SA +730,1840,000,15,cl,Chile,56,TESAM SA +730,1840,013,319,cl,Chile,56,Tribe Mobile SPA +730,1840,008,143,cl,Chile,56,VTR Banda Ancha SA +460,1120,000,15,cn,China,86,China Mobile GSM +460,1120,007,127,cn,China,86,China Mobile GSM2 +460,1120,002,47,cn,China,86,China Mobile GSM3 +460,1120,004,79,cn,China,86,China Space Mobile Satellite Telecommunications Co. Ltd (China Spacecom) +460,1120,005,5,cn,China,86,China Telecom +460,1120,003,63,cn,China,86,China Telecom2 +460,1120,006,6,cn,China,86,China Unicom +460,1120,001,31,cn,China,86,China Unicom2 +732,1842,130,304,co,Colombia,57,Avantel SAS +732,1842,102,258,co,Colombia,57,Movistar +732,1842,103,259,co,Colombia,57,TIGO/Colombia Movil +732,1842,001,1,co,Colombia,57,TIGO/Colombia Movil +732,1842,101,257,co,Colombia,57,Comcel S.A. Occel S.A./Celcaribe +732,1842,002,2,co,Colombia,57,Edatel S.A. +732,1842,123,291,co,Colombia,57,Movistar +732,1842,111,273,co,Colombia,57,TIGO/Colombia Movil +732,1842,142,322,co,Colombia,57,UNE EPM Telecomunicaciones SA ESP +732,1842,020,32,co,Colombia,57,UNE EPM Telecomunicaciones SA ESP2 +732,1842,154,340,co,Colombia,57,Virgin Mobile Colombia SAS +654,1620,001,31,km,Comoros,269,HURI - SNPT +630,1584,086,2159,cd,Congo Dem. Rep.,243,Orange RDC sarl +630,1584,005,95,cd,Congo Dem. Rep.,243,SuperCell +630,1584,089,2207,cd,Congo Dem. Rep.,243,TIGO/Oasis +630,1584,001,31,cd,Congo Dem. Rep.,243,Vodacom +630,1584,088,2191,cd,Congo Dem. Rep.,243,Yozma Timeturns sprl (YTT) +630,1584,002,47,cd,Congo Dem. Rep.,243,ZAIN CelTel +629,1577,001,31,cg,Congo Republic,242,Airtel Congo SA +629,1577,002,2,cg,Congo Republic,242,Zain/Celtel +629,1577,010,271,cg,Congo Republic,242,MTN/Libertis +629,1577,007,127,cg,Congo Republic,242,Warid +548,1352,001,31,ck,Cook Islands,682,Telecom Cook Islands +712,1810,003,63,cr,Costa Rica,506,Claro +712,1810,002,47,cr,Costa Rica,506,ICE +712,1810,001,31,cr,Costa Rica,506,ICE2 +712,1810,004,79,cr,Costa Rica,506,Movistar +219,537,001,31,hr,Croatia,385,T-Mobile/Cronet +219,537,002,47,hr,Croatia,385,Tele2 +219,537,010,271,hr,Croatia,385,VIPnet d.o.o. +368,872,001,31,cu,Cuba,53,C-COM +362,866,095,2399,cw,Curacao,,EOCG Wireless NV +362,866,069,1695,cw,Curacao,,Polycom N.V./ Curacao Telecom d.b.a. Digicel +280,640,010,271,cy,Cyprus,357,MTN/Areeba +280,640,020,527,cy,Cyprus,357,PrimeTel PLC +280,640,001,31,cy,Cyprus,357,Vodafone/CyTa +230,560,008,143,cz,Czech Rep.,420,Compatel s.r.o. +230,560,002,47,cz,Czech Rep.,420,O2 +230,560,001,31,cz,Czech Rep.,420,T-Mobile / RadioMobil +230,560,005,95,cz,Czech Rep.,420,Travel Telekommunikation s.r.o. +230,560,004,79,cz,Czech Rep.,420,Ufone +230,560,099,2463,cz,Czech Rep.,420,Vodafone +230,560,003,63,cz,Czech Rep.,420,Vodafone +238,568,005,5,dk,Denmark,45,ApS KBUS +238,568,023,575,dk,Denmark,45,Banedanmark +238,568,028,655,dk,Denmark,45,CoolTEL ApS +238,568,006,111,dk,Denmark,45,Hi3G +238,568,012,303,dk,Denmark,45,Lycamobile Ltd +238,568,003,63,dk,Denmark,45,Mach Connectivity ApS +238,568,007,127,dk,Denmark,45, +238,568,004,79,dk,Denmark,45,NextGen Mobile Ltd (CardBoardFish) +238,568,010,271,dk,Denmark,45,TDC Denmark +238,568,001,31,dk,Denmark,45,TDC Denmark2 +238,568,002,47,dk,Denmark,45,Telenor/Sonofon +238,568,077,1919,dk,Denmark,45,Telenor/Sonofon +238,568,020,527,dk,Denmark,45,Telia +238,568,030,783,dk,Denmark,45,Telia2 +638,1592,001,31,dj,Djibouti,253,Djibouti Telecom SA (Evatis) +366,870,110,272,dm,Dominica,1767,C & W +366,870,020,32,dm,Dominica,1767,Cingular Wireless/Digicel +366,870,050,80,dm,Dominica,1767,Wireless Ventures (Dominica) Ltd (Digicel Dominica) +370,880,002,47,do,Dominican Republic,1809,Claro +370,880,001,31,do,Dominican Republic,1809,Orange +370,880,003,63,do,Dominican Republic,1809,TRIcom +370,880,004,79,do,Dominican Republic,1809,Trilogy Dominicana S. A. +740,1856,002,47,ec,Ecuador,593,Alegro/Telcsa +740,1856,000,15,ec,Ecuador,593,MOVISTAR/OteCel +740,1856,001,31,ec,Ecuador,593,Porta/Conecel +602,1538,001,31,eg,Egypt,20,EMS - Mobinil +602,1538,003,63,eg,Egypt,20,ETISALAT +602,1538,002,47,eg,Egypt,20,Vodafone (Misrfone Telecom) +706,1798,001,31,sv,El Salvador,503,CLARO/CTE +706,1798,002,47,sv,El Salvador,503,Digicel +706,1798,005,95,sv,El Salvador,503,INTELFON SA de CV +706,1798,004,79,sv,El Salvador,503,Telefonica +706,1798,003,63,sv,El Salvador,503,Telemovil +627,1575,003,63,gq,Equatorial Guinea,240,HiTs-GE +627,1575,001,31,gq,Equatorial Guinea,240,ORANGE/GETESA +657,1623,000,0,er,Eritrea,291,EriTel +657,1623,001,31,er,Eritrea,291,Eritel +248,584,001,31,ee,Estonia,372,EMT GSM +248,584,002,47,ee,Estonia,372,Radiolinja Eesti +248,584,003,63,ee,Estonia,372,Tele2 Eesti AS +248,584,004,79,ee,Estonia,372,Top Connect OU +636,1590,001,31,et,Ethiopia,251,ETH/MTN +750,1872,001,1,fk,Falkland Islands (Malvinas),,Cable and Wireless South Atlantic Ltd (Falkland Islands +288,648,003,63,fo,Faroe Islands,298,Edge Mobile Sp/F +288,648,001,31,fo,Faroe Islands,298,Faroese Telecom +288,648,002,47,fo,Faroe Islands,298,Kall GSM +542,1346,002,47,fj,Fiji,679,DigiCell +542,1346,001,31,fj,Fiji,679,Vodafone +244,580,014,335,fi,Finland,358,Alands +244,580,026,623,fi,Finland,358,Compatel Ltd +244,580,013,319,fi,Finland,358,DNA/Finnet +244,580,012,303,fi,Finland,358,DNA/Finnet2 +244,580,004,79,fi,Finland,358,DNA/Finnet +244,580,003,63,fi,Finland,358,DNA/Finnet2 +244,580,021,543,fi,Finland,358,Elisa/Saunalahti +244,580,005,95,fi,Finland,358,Elisa/Saunalahti2 +244,580,082,2095,fi,Finland,358,ID-Mobile +244,580,011,287,fi,Finland,358,Mundio Mobile (Finland) Ltd +244,580,009,159,fi,Finland,358,Nokia Oyj +244,580,010,271,fi,Finland,358,TDC Oy Finland +244,580,091,2335,fi,Finland,358,TeliaSonera +208,520,027,639,fr,France,33,AFONE SA +208,520,092,2351,fr,France,33,Association Plate-forme Telecom +208,520,028,655,fr,France,33,Astrium +208,520,088,2191,fr,France,33,Bouygues Telecom +208,520,021,543,fr,France,33,Bouygues Telecom2 +208,520,020,527,fr,France,33,Bouygues Telecom3 +208,520,014,335,fr,France,33,Lliad/FREE Mobile +208,520,005,95,fr,France,33,GlobalStar +208,520,007,127,fr,France,33,GlobalStar2 +208,520,006,111,fr,France,33,GlobalStar3 +208,520,029,671,fr,France,33,Orange +208,520,016,367,fr,France,33,Lliad/FREE Mobile +208,520,015,351,fr,France,33,Lliad/FREE Mobile2 +208,520,025,607,fr,France,33,Lycamobile SARL +208,520,024,591,fr,France,33,MobiquiThings +208,520,003,63,fr,France,33,MobiquiThings2 +208,520,031,799,fr,France,33,Mundio Mobile (France) Ltd +208,520,026,623,fr,France,33,NRJ +208,520,089,2207,fr,France,33,Omer/Virgin Mobile +208,520,023,575,fr,France,33,Omer/Virgin Mobile2 +208,520,002,47,fr,France,33,Orange +208,520,001,31,fr,France,33,Orange2 +208,520,091,2335,fr,France,33,Orange3 +208,520,013,319,fr,France,33,S.F.R. +208,520,011,287,fr,France,33,S.F.R.2 +208,520,010,271,fr,France,33,S.F.R.3 +208,520,009,159,fr,France,33,S.F.R.4 +208,520,004,79,fr,France,33,SISTEER +208,520,000,15,fr,France,33,Tel/Tel +208,520,022,559,fr,France,33,Transatel SA +340,832,020,527,gf,French Guiana,594,Bouygues/DigiCel +340,832,008,143,gf,French Guiana,594,AMIGO/Dauphin +340,832,001,31,gf,French Guiana,594,Orange Caribe +340,832,002,47,gf,French Guiana,594,Outremer Telecom +340,832,011,287,gf,French Guiana,594,TelCell GSM +340,832,003,63,gf,French Guiana,594,TelCell GSM2 +547,1351,015,351,pf,French Polynesia,689,Pacific Mobile Telecom (PMT) +547,1351,020,527,pf,French Polynesia,689,Tikiphone +628,1576,004,79,ga,Gabon,241,Azur/Usan S.A. +628,1576,001,31,ga,Gabon,241,Libertis S.A. +628,1576,002,47,ga,Gabon,241,MOOV/Telecel +628,1576,003,63,ga,Gabon,241,ZAIN/Celtel Gabon S.A. +607,1543,002,47,gm,Gambia,220,Africel +607,1543,003,63,gm,Gambia,220,Comium +607,1543,001,31,gm,Gambia,220,Gamcel +607,1543,004,79,gm,Gambia,220,Q-Cell +282,642,001,31,ge,Georgia,995,Geocell Ltd. +282,642,003,3,ge,Georgia,995,Iberiatel Ltd. +282,642,002,47,ge,Georgia,995,Magti GSM Ltd. +282,642,004,79,ge,Georgia,995,MobiTel/Beeline +282,642,000,0,ge,Georgia,995,Silknet +262,610,017,383,de,Germany,49,E-Plus +262,610,003,63,de,Germany,49,E-Plus2 +262,610,005,95,de,Germany,49,E-Plus3 +262,610,077,1919,de,Germany,49,E-Plus4 +262,610,014,335,de,Germany,49,Group 3G UMTS +262,610,043,1087,de,Germany,49,Lycamobile +262,610,013,319,de,Germany,49,Mobilcom +262,610,007,127,de,Germany,49,O2 +262,610,011,287,de,Germany,49,O2-2 +262,610,008,143,de,Germany,49,O2-3 +262,610,010,271,de,Germany,49,O2-4 +262,610,012,303,de,Germany,49,O2-5 +262,610,006,111,de,Germany,49,Telekom/T-mobile +262,610,001,31,de,Germany,49,Telekom/T-mobile2 +262,610,016,367,de,Germany,49,Telogic/ViStream +262,610,004,79,de,Germany,49,Vodafone D2 +262,610,002,47,de,Germany,49,Vodafone D2-2 +262,610,009,159,de,Germany,49,Vodafone D2-3 +620,1568,004,79,gh,Ghana,233,Expresso Ghana Ltd +620,1568,007,127,gh,Ghana,233,GloMobile +620,1568,003,63,gh,Ghana,233,Milicom/Tigo +620,1568,001,31,gh,Ghana,233,MTN +620,1568,002,47,gh,Ghana,233,Vodafone +620,1568,006,111,gh,Ghana,233,ZAIN +266,614,006,111,gi,Gibraltar,350,CTS Mobile +266,614,009,159,gi,Gibraltar,350,eazi telecom +266,614,001,31,gi,Gibraltar,350,Gibtel GSM +202,514,007,127,gr,Greece,30,AMD Telecom SA +202,514,002,47,gr,Greece,30,Cosmote +202,514,001,31,gr,Greece,30,Cosmote2 +202,514,004,79,gr,Greece,30,Organismos Sidirodromon Ellados (OSE) +202,514,003,63,gr,Greece,30,OTE Hellenic Telecommunications Organization SA +202,514,010,271,gr,Greece,30,Tim/Wind +202,514,009,159,gr,Greece,30,Tim/Wind2 +202,514,005,95,gr,Greece,30,Vodafone +290,656,001,31,gl,Greenland,299,Tele Greenland +352,850,110,272,gd,Grenada,1473,Cable & Wireless +352,850,030,48,gd,Grenada,1473,Digicel +352,850,050,80,gd,Grenada,1473,Digicel2 +340,832,008,143,gp,Guadeloupe,590,Dauphin Telecom SU (Guadeloupe Telecom) (Guadeloupe) +340,832,020,527,gp,Guadeloupe,590,Digicel Antilles Francaises Guyane SA (Guadeloupe) +340,832,001,31,gp,Guadeloupe,590,Orange Caribe +340,832,002,47,gp,Guadeloupe,590,Outremer Telecom Guadeloupe (only) (Guadeloupe) +340,832,010,271,gp,Guadeloupe,590,United Telecommunications Services Caraibe SARL (UTS Caraibe Guadeloupe Telephone Mobile) (Guadeloupe) +340,832,003,63,gp,Guadeloupe,590,United Telecommunications Services Caraibe SARL (UTS Caraibe Guadeloupe Telephone Mobile) (Guadeloupe)2 +310,784,480,1152,gu,Guam,1671,Choice Phone LLC +310,784,370,880,gu,Guam,1671,Docomo +310,784,470,1136,gu,Guam,1671,Docomo2 +310,784,140,320,gu,Guam,1671,GTA Wireless +310,784,033,51,gu,Guam,1671,Guam Teleph. Auth. +310,784,032,50,gu,Guam,1671,IT&E OverSeas +311,785,250,592,gu,Guam,1671,Wave Runner LLC +704,1796,001,31,gt,Guatemala,502,SERCOM +704,1796,003,63,gt,Guatemala,502,Telefonica +704,1796,002,47,gt,Guatemala,502,TIGO/COMCEL +611,1553,004,4,gn,Guinea,224,Areeba - MTN +611,1553,005,95,gn,Guinea,224,Celcom +611,1553,003,63,gn,Guinea,224,Intercel +611,1553,001,31,gn,Guinea,224,Orange/Spacetel +611,1553,002,47,gn,Guinea,224,SotelGui +632,1586,000,0,gw,Guinea-Bissau,245,GuineTel +632,1586,001,31,gw,Guinea-Bissau,245,GuineTel2 +632,1586,003,63,gw,Guinea-Bissau,245,Orange +632,1586,002,47,gw,Guinea-Bissau,245,SpaceTel +738,1848,002,47,gy,Guyana,592,Cellink Plus +738,1848,001,31,gy,Guyana,592,DigiCel +372,882,001,31,ht,Haiti,509,Comcel +372,882,002,47,ht,Haiti,509,Digicel +372,882,003,63,ht,Haiti,509,National Telecom SA (NatCom) +708,1800,040,64,hn,Honduras,504,Digicel +708,1800,030,48,hn,Honduras,504,HonduTel +708,1800,001,1,hn,Honduras,504,SERCOM/CLARO +708,1800,002,2,hn,Honduras,504,Telefonica/CELTEL +454,1108,013,319,hk,Hongkong China,852,China Mobile/Peoples +454,1108,012,303,hk,Hongkong China,852,China Mobile/Peoples2 +454,1108,009,159,hk,Hongkong China,852,China Motion +454,1108,007,127,hk,Hongkong China,852,China Unicom Ltd +454,1108,011,287,hk,Hongkong China,852,China-HongKong Telecom Ltd (CHKTL) +454,1108,001,31,hk,Hongkong China,852,Citic Telecom Ltd. +454,1108,018,399,hk,Hongkong China,852,CSL Ltd. +454,1108,002,47,hk,Hongkong China,852,CSL Ltd.2 +454,1108,000,15,hk,Hongkong China,852,CSL Ltd.3 +454,1108,010,271,hk,Hongkong China,852,CSL/New World PCS Ltd. +454,1108,014,335,hk,Hongkong China,852,H3G/Hutchinson +454,1108,005,95,hk,Hongkong China,852,H3G/Hutchinson2 +454,1108,004,79,hk,Hongkong China,852,H3G/Hutchinson3 +454,1108,003,63,hk,Hongkong China,852,H3G/Hutchinson4 +454,1108,016,367,hk,Hongkong China,852,HKT/PCCW +454,1108,019,415,hk,Hongkong China,852,HKT/PCCW2 +454,1108,020,527,hk,Hongkong China,852,HKT/PCCW3 +454,1108,029,671,hk,Hongkong China,852,HKT/PCCW4 +454,1108,047,1151,hk,Hongkong China,852,shared by private TETRA systems +454,1108,040,1039,hk,Hongkong China,852,shared by private TETRA systems2 +454,1108,008,143,hk,Hongkong China,852,Trident Telecom Ventures Ltd. +454,1108,017,383,hk,Hongkong China,852,Vodafone/SmarTone +454,1108,015,351,hk,Hongkong China,852,Vodafone/SmarTone2 +454,1108,006,111,hk,Hongkong China,852,Vodafone/SmarTone3 +216,534,001,31,hu,Hungary,36,Pannon/Telenor +216,534,030,783,hu,Hungary,36,T-mobile/Magyar +216,534,071,1823,hu,Hungary,36,UPC Magyarorszag Kft. +216,534,070,1807,hu,Hungary,36,Vodafone +274,628,009,159,is,Iceland,354,Amitelo +274,628,007,127,is,Iceland,354,IceCell +274,628,008,143,is,Iceland,354,Landssiminn +274,628,001,31,is,Iceland,354,Landssiminn2 +274,628,011,287,is,Iceland,354,NOVA +274,628,004,79,is,Iceland,354,VIKING/IMC +274,628,003,63,is,Iceland,354,Vodafone/Tal hf +274,628,005,95,is,Iceland,354,Vodafone/Tal hf2 +274,628,002,47,is,Iceland,354,Vodafone/Tal hf3 +404,1028,029,671,in,India,91,Aircel +404,1028,028,655,in,India,91,Aircel2 +404,1028,025,607,in,India,91,Aircel3 +404,1028,017,383,in,India,91,Aircel4 +404,1028,042,1071,in,India,91,Aircel5 +404,1028,033,831,in,India,91,Aircel6 +404,1028,001,1,in,India,91,Aircel Digilink India +404,1028,015,351,in,India,91,Aircel Digilink India-2 +404,1028,060,1551,in,India,91,Aircel Digilink India-3 +404,1028,049,000,in,India,91,AirTel +405,1029,055,1375,in,India,91,AirTel2 +405,1029,054,1375,in,India,91,AirTel3 +405,1029,053,1343,in,India,91,AirTel4 +405,1029,051,1311,in,India,91,AirTel5 +405,1029,056,1391,in,India,91,Airtel (Bharati Mobile) - Assam +404,1028,086,2159,in,India,91,Barakhamba Sales & Serv. +404,1028,013,319,in,India,91,Vodafone +404,1028,011,000,in,India,91,Vodafone2 +404,1028,058,1423,in,India,91,BSNL +404,1028,081,2079,in,India,91,BSNL2 +404,1028,074,1871,in,India,91,BSNL3 +404,1028,038,911,in,India,91,BSNL4 +404,1028,057,1407,in,India,91,BSNL5 +404,1028,080,2063,in,India,91,BSNL6 +404,1028,073,1855,in,India,91,BSNL7 +404,1028,034,847,in,India,91,BSNL8 +404,1028,066,1647,in,India,91,BSNL9 +404,1028,055,1375,in,India,91,BSNL10 +404,1028,072,1839,in,India,91,BSNL11 +404,1028,077,1919,in,India,91,BSNL12 +404,1028,064,1615,in,India,91,BSNL13 +404,1028,054,1359,in,India,91,BSNL14 +404,1028,071,1823,in,India,91,BSNL15 +404,1028,076,1903,in,India,91,BSNL16 +404,1028,053,1343,in,India,91,BSNL17 +404,1028,062,1583,in,India,91,BSNL18 +404,1028,059,1439,in,India,91,BSNL19 +404,1028,075,1887,in,India,91,BSNL20 +404,1028,051,1311,in,India,91,BSNL21 +405,1028,001,000,in,INDIA,91,Reliance +405,1028,025,000,in,India,91,TATA DOCOMO +405,1028,036,000,in,India,91,TATA DOCOMO2 +405,1029,010,271,in,India,91,Bharti Airtel Limited (Delhi) +404,1028,079,1951,in,India,91,CellOne A&N +404,1028,089,2207,in,India,91,Escorts Telecom Ltd. +404,1028,088,2191,in,India,91,Escorts Telecom Ltd.2 +404,1028,087,2175,in,India,91,Escorts Telecom Ltd.3 +404,1028,082,2095,in,India,91,Escorts Telecom Ltd.4 +404,1028,012,303,in,India,91,Escotel Mobile Communications +404,1028,019,415,in,India,91,Escotel Mobile Communications2 +404,1028,056,1391,in,India,91,Escotel Mobile Communications3 +405,1029,005,5,in,India,91,Fascel Limited +404,1028,005,5,in,India,91,Fascel +404,1028,070,1807,in,India,91,Hexacom India +404,1028,016,367,in,India,91,Hexcom India2 +404,1028,004,4,in,India,91,Idea Cellular Ltd. +404,1028,024,591,in,India,91,Idea Cellular Ltd. +404,1028,022,559,in,India,91,Idea Cellular Ltd.2 +404,1028,078,1935,in,India,91,Idea Cellular Ltd.3 +404,1028,007,7,in,India,91,Idea Cellular Ltd.4 +404,1028,069,1695,in,India,91,Mahanagar Telephone Nigam +404,1028,068,1679,in,India,91,Mahanagar Telephone Nigam2 +404,1028,083,2111,in,India,91,Reliable Internet Services +405,1029,009,9,in,India,91,RELIANCE TELECOM +404,1028,036,879,in,India,91,Reliance Telecom Private +404,1028,052,1327,in,India,91,Reliance Telecom Private2 +404,1028,050,1295,in,India,91,Reliance Telecom Private3 +404,1028,067,1663,in,India,91,Reliance Telecom Private4 +404,1028,018,399,in,India,91,Reliance Telecom Private5 +404,1028,085,2143,in,India,91,Reliance Telecom Private6 +404,1028,009,9,in,India,91,Reliance Telecom Private7 +404,1028,041,1055,in,India,91,RPG Cellular +404,1028,014,335,in,India,91,Spice +404,1028,044,1103,in,India,91,Spice2 +404,1028,011,287,in,India,91,Sterling Cellular Ltd. +404,1028,030,783,in,India,91,Usha Martin Telecom +510,1296,008,143,id,Indonesia,62,Axis/Natrindo +510,1296,089,2207,id,Indonesia,62,H3G CP +510,1296,021,543,id,Indonesia,62,Indosat/Satelindo/M3 +510,1296,001,31,id,Indonesia,62,Indosat/Satelindo/M3 +510,1296,000,0,id,Indonesia,62,PT Pasifik Satelit Nusantara (PSN) +510,1296,000,15,id,Indonesia,62,PT Pasifik Satelit Nusantara (PSN)2 +510,1296,027,639,id,Indonesia,62,PT Sampoerna Telekomunikasi Indonesia (STI) +510,1296,009,159,id,Indonesia,62,PT Smartfren Telecom Tbk +510,1296,028,655,id,Indonesia,62,PT Smartfren Telecom Tbk2 +510,1296,011,287,id,Indonesia,62,PT. Excelcom +510,1296,010,271,id,Indonesia,62,Telkomsel +901,2305,013,319,n/a,International Networks,882,Antarctica +432,1074,019,415,ir,Iran,98,Mobile Telecommunications Company of Esfahan JV-PJS (MTCE) +432,1074,070,1807,ir,Iran,98,MTCE +432,1074,035,863,ir,Iran,98,MTN/IranCell +432,1074,032,815,ir,Iran,98,Taliya +432,1074,011,287,ir,Iran,98,TCI / MCI +432,1074,014,335,ir,Iran,98,TKC/KFZO +418,1048,005,95,iq,Iraq,964,Asia Cell +418,1048,092,2351,iq,Iraq,964,Itisaluna and Kalemat +418,1048,082,2095,iq,Iraq,964,Korek +418,1048,040,1039,iq,Iraq,964,Korek2 +418,1048,045,1119,iq,Iraq,964,Mobitel (Iraq-Kurdistan) and Moutiny +418,1048,030,783,iq,Iraq,964,Orascom Telecom +418,1048,020,527,iq,Iraq,964,ZAIN/Atheer +418,1048,008,8,iq,Iraq,964,Sanatel +272,626,004,79,ie,Ireland,353,Access Telecom Ltd. +272,626,009,159,ie,Ireland,353,Clever Communications Ltd +272,626,007,127,ie,Ireland,353,eircom Ltd +272,626,005,95,ie,Ireland,353,H3G +272,626,011,287,ie,Ireland,353,Liffey Telecom +272,626,013,319,ie,Ireland,353,Lycamobile +272,626,003,63,ie,Ireland,353,Meteor Mobile Ltd. +272,626,002,47,ie,Ireland,353,O2/Digifone +272,626,001,31,ie,Ireland,353,Vodafone Eircell +425,1061,014,335,il,Israel,972,Alon Cellular Ltd +425,1061,002,47,il,Israel,972,Cellcom ltd. +425,1061,008,143,il,Israel,972,Golan Telekom +425,1061,015,351,il,Israel,972,Home Cellular Ltd +425,1061,077,1919,il,Israel,972,Hot Mobile/Mirs +425,1061,007,127,il,Israel,972,Hot Mobile/Mirs2 +425,1061,001,31,il,Israel,972,Orange/Partner Co. Ltd. +425,1061,003,63,il,Israel,972,Pelephone +425,1061,016,367,il,Israel,972,Rami Levy Hashikma Marketing Communications Ltd +222,546,034,847,it,Italy,39,BT Italia SpA +222,546,002,47,it,Italy,39,Elsacom +222,546,099,2463,it,Italy,39,Hi3G +222,546,033,831,it,Italy,39,Hi3G-2 +222,546,077,1919,it,Italy,39,IPSE 2000 +222,546,035,863,it,Italy,39,Lycamobile Srl +222,546,007,127,it,Italy,39,Noverca Italia Srl +222,546,030,783,it,Italy,39,RFI Rete Ferroviaria Italiana SpA +222,546,048,1167,it,Italy,39,Telecom Italia Mobile SpA +222,546,043,1087,it,Italy,39,Telecom Italia Mobile SpA2 +222,546,001,31,it,Italy,39,TIM +222,546,010,271,it,Italy,39,Vodafone +222,546,006,111,it,Italy,39,Vodafone +222,546,044,1103,it,Italy,39,WIND (Blu) +222,546,088,2191,it,Italy,39,WIND (Blu)2 +612,1554,007,127,ci,Ivory Coast,225,Aircomm SA +612,1554,002,47,ci,Ivory Coast,225,Atlantik Tel./Moov +612,1554,004,79,ci,Ivory Coast,225,Comium +612,1554,001,1,ci,Ivory Coast,225,Comstar +612,1554,005,95,ci,Ivory Coast,225,MTN +612,1554,003,63,ci,Ivory Coast,225,Orange +612,1554,006,111,ci,Ivory Coast,225,OriCell +612,1554,000,0,ci,Ivory Coast,225,Warid +338,824,110,272,jm,Jamaica,1876,Cable & Wireless +338,824,020,32,jm,Jamaica,1876,Cable & Wireless-2 +338,824,180,384,jm,Jamaica,1876,Cable & Wireless-3 +338,824,050,80,jm,Jamaica,1876,DIGICEL/Mossel +440,1088,000,0,jp,Japan,81,eMobile +440,1088,074,1871,jp,Japan,81,KDDI Corporation +440,1088,070,1807,jp,Japan,81,KDDI Corporation2 +440,1088,089,2207,jp,Japan,81,KDDI Corporation3 +440,1088,051,1311,jp,Japan,81,KDDI Corporation4 +440,1088,075,1887,jp,Japan,81,KDDI Corporation5 +440,1088,056,1391,jp,Japan,81,KDDI Corporation6 +441,1089,070,1807,jp,Japan,81,KDDI Corporation7 +440,1088,052,1327,jp,Japan,81,KDDI Corporation8 +440,1088,076,1903,jp,Japan,81,KDDI Corporation9 +440,1088,071,1823,jp,Japan,81,KDDI Corporation10 +440,1088,053,1343,jp,Japan,81,KDDI Corporation11 +440,1088,077,1919,jp,Japan,81,KDDI Corporation12 +440,1088,008,143,jp,Japan,81,KDDI Corporation13 +440,1088,072,1839,jp,Japan,81,KDDI Corporation14 +440,1088,054,1359,jp,Japan,81,KDDI Corporation15 +440,1088,079,1951,jp,Japan,81,KDDI Corporation16 +440,1088,007,127,jp,Japan,81,KDDI Corporation17 +440,1088,073,1855,jp,Japan,81,KDDI Corporation18 +440,1088,055,1375,jp,Japan,81,KDDI Corporation19 +440,1088,088,2191,jp,Japan,81,KDDI Corporation20 +440,1088,050,1295,jp,Japan,81,KDDI Corporation21 +440,1088,021,543,jp,Japan,81,NTT Docomo +441,1089,044,1103,jp,Japan,81,NTT Docomo2 +440,1088,013,319,jp,Japan,81,NTT Docomo3 +440,1088,023,575,jp,Japan,81,NTT Docomo4 +440,1088,016,367,jp,Japan,81,NTT Docomo5 +441,1089,099,2463,jp,Japan,81,NTT Docomo6 +440,1088,034,847,jp,Japan,81,NTT Docomo7 +440,1088,069,1695,jp,Japan,81,NTT Docomo8 +440,1088,064,1615,jp,Japan,81,NTT Docomo9 +440,1088,037,895,jp,Japan,81,NTT Docomo10 +440,1088,025,607,jp,Japan,81,NTT Docomo11 +440,1088,022,559,jp,Japan,81,NTT Docomo12 +441,1089,043,1087,jp,Japan,81,NTT Docomo13 +440,1088,027,639,jp,Japan,81,NTT Docomo14 +440,1088,002,47,jp,Japan,81,NTT Docomo15 +440,1088,017,383,jp,Japan,81,NTT Docomo16 +440,1088,031,799,jp,Japan,81,NTT Docomo17 +440,1088,087,2175,jp,Japan,81,NTT Docomo18 +440,1088,065,1631,jp,Japan,81,NTT Docomo19 +440,1088,036,879,jp,Japan,81,NTT Docomo20 +441,1089,092,2351,jp,Japan,81,NTT Docomo21 +440,1088,012,303,jp,Japan,81,NTT Docomo22 +440,1088,058,1423,jp,Japan,81,NTT Docomo23 +440,1088,028,655,jp,Japan,81,NTT Docomo24 +440,1088,003,63,jp,Japan,81,NTT Docomo25 +440,1088,018,399,jp,Japan,81,NTT Docomo26 +441,1089,091,2335,jp,Japan,81,NTT Docomo27 +440,1088,032,815,jp,Japan,81,NTT Docomo28 +440,1088,061,1567,jp,Japan,81,NTT Docomo29 +440,1088,066,1647,jp,Japan,81,NTT Docomo30 +440,1088,035,863,jp,Japan,81,NTT Docomo31 +441,1089,093,2367,jp,Japan,81,NTT Docomo32 +441,1089,040,1039,jp,Japan,81,NTT Docomo33 +440,1088,049,1183,jp,Japan,81,NTT Docomo34 +440,1088,029,671,jp,Japan,81,NTT Docomo35 +440,1088,009,159,jp,Japan,81,NTT Docomo36 +440,1088,019,415,jp,Japan,81,NTT Docomo37 +441,1089,090,2319,jp,Japan,81,NTT Docomo38 +440,1088,033,831,jp,Japan,81,NTT Docomo39 +440,1088,060,1551,jp,Japan,81,NTT Docomo40 +440,1088,014,335,jp,Japan,81,NTT Docomo41 +441,1089,094,2383,jp,Japan,81,NTT Docomo42 +441,1089,041,1055,jp,Japan,81,NTT Docomo43 +440,1088,067,1663,jp,Japan,81,NTT Docomo44 +440,1088,062,1583,jp,Japan,81,NTT Docomo45 +440,1088,001,31,jp,Japan,81,NTT Docomo46 +440,1088,039,927,jp,Japan,81,NTT Docomo47 +440,1088,030,783,jp,Japan,81,NTT Docomo48 +440,1088,010,271,jp,Japan,81,NTT Docomo49 +440,1088,020,527,jp,Japan,81,NTT Docomo50 +441,1089,045,1119,jp,Japan,81,NTT Docomo51 +440,1088,024,591,jp,Japan,81,NTT Docomo52 +440,1088,015,351,jp,Japan,81,NTT Docomo53 +441,1089,098,2447,jp,Japan,81,NTT Docomo54 +441,1089,042,1071,jp,Japan,81,NTT Docomo55 +440,1088,068,1679,jp,Japan,81,NTT Docomo56 +440,1088,063,1599,jp,Japan,81,NTT Docomo57 +440,1088,038,911,jp,Japan,81,NTT Docomo58 +440,1088,026,623,jp,Japan,81,NTT Docomo59 +440,1088,011,287,jp,Japan,81,NTT Docomo60 +440,1088,099,2463,jp,Japan,81,NTT Docomo61 +440,1088,078,1935,jp,Japan,81,Okinawa Cellular Telephone +440,1088,047,1151,jp,Japan,81,SoftBank Mobile Corp +440,1088,095,2399,jp,Japan,81,SoftBank Mobile Corp2 +440,1088,041,1055,jp,Japan,81,SoftBank Mobile Corp3 +441,1089,064,1615,jp,Japan,81,SoftBank Mobile Corp4 +440,1088,046,1135,jp,Japan,81,SoftBank Mobile Corp5 +440,1088,097,2431,jp,Japan,81,SoftBank Mobile Corp6 +440,1088,042,1071,jp,Japan,81,SoftBank Mobile Corp7 +441,1089,065,1631,jp,Japan,81,SoftBank Mobile Corp8 +440,1088,090,2319,jp,Japan,81,SoftBank Mobile Corp9 +440,1088,092,2351,jp,Japan,81,SoftBank Mobile Corp10 +440,1088,098,2447,jp,Japan,81,SoftBank Mobile Corp11 +440,1088,043,1087,jp,Japan,81,SoftBank Mobile Corp12 +440,1088,048,1167,jp,Japan,81,SoftBank Mobile Corp13 +440,1088,093,2367,jp,Japan,81,SoftBank Mobile Corp14 +440,1088,006,111,jp,Japan,81,SoftBank Mobile Corp15 +441,1089,061,1567,jp,Japan,81,SoftBank Mobile Corp16 +440,1088,044,1103,jp,Japan,81,SoftBank Mobile Corp17 +440,1088,004,79,jp,Japan,81,SoftBank Mobile Corp18 +440,1088,094,2383,jp,Japan,81,SoftBank Mobile Corp19 +441,1089,062,1583,jp,Japan,81,SoftBank Mobile Corp20 +440,1088,045,1119,jp,Japan,81,SoftBank Mobile Corp21 +440,1088,040,1039,jp,Japan,81,SoftBank Mobile Corp22 +440,1088,096,2415,jp,Japan,81,SoftBank Mobile Corp23 +441,1089,063,1599,jp,Japan,81,SoftBank Mobile Corp24 +440,1088,085,2143,jp,Japan,81,KDDI Corporation +440,1088,083,2111,jp,Japan,81,KDDI Corporation2 +440,1088,081,2079,jp,Japan,81,KDDI Corporation3 +440,1088,080,2063,jp,Japan,81,KDDI Corporation4 +440,1088,086,2159,jp,Japan,81,KDDI Corporation5 +440,1088,084,2127,jp,Japan,81,KDDI Corporation6 +440,1088,082,2095,jp,Japan,81,KDDI Corporation7 +416,1046,077,1919,jo,Jordan,962,Orange/Petra +416,1046,003,63,jo,Jordan,962,Umniah Mobile Co. +416,1046,002,2,jo,Jordan,962,Xpress +416,1046,001,31,jo,Jordan,962,ZAIN /J.M.T.S +401,1025,001,31,kz,Kazakhstan,7,Beeline/KaR-Tel LLP +401,1025,007,127,kz,Kazakhstan,7,Dalacom/Altel +401,1025,002,47,kz,Kazakhstan,7,K-Cell +401,1025,077,1919,kz,Kazakhstan,7,NEO/MTS +639,1593,005,95,ke,Kenya,254,Econet Wireless +639,1593,007,127,ke,Kenya,254,Orange +639,1593,002,47,ke,Kenya,254,Safaricom Ltd. +639,1593,003,63,ke,Kenya,254,Zain/Celtel Ltd. +545,1349,009,9,ki,Kiribati,686,Kiribati Frigate +467,1127,193,403,kp,Korea N. Dem. People's Rep.,850,Sun Net +450,1104,002,47,kr,Korea S Republic of,82,KT Freetel Co. Ltd. +450,1104,004,79,kr,Korea S Republic of,82,KT Freetel Co. Ltd.2 +450,1104,008,143,kr,Korea S Republic of,82,KT Freetel Co. Ltd.3 +450,1104,006,111,kr,Korea S Republic of,82,LG Telecom +450,1104,003,63,kr,Korea S Republic of,82,SK Telecom +450,1104,005,95,kr,Korea S Republic of,82,SK Telecom Co. Ltd +419,1049,004,79,kw,Kuwait,965,Viva +419,1049,003,63,kw,Kuwait,965,Wantaniya +419,1049,002,47,kw,Kuwait,965,Zain +437,1079,003,63,kg,Kyrgyzstan,996,AkTel LLC +437,1079,001,31,kg,Kyrgyzstan,996,Beeline/Bitel +437,1079,005,95,kg,Kyrgyzstan,996,MEGACOM +437,1079,009,159,kg,Kyrgyzstan,996,O!/NUR Telecom +457,1111,002,47,la,Laos P.D.R.,856,ETL Mobile +457,1111,001,31,la,Laos P.D.R.,856,Lao Tel +457,1111,008,143,la,Laos P.D.R.,856,Tigo/Millicom +457,1111,003,63,la,Laos P.D.R.,856,UNITEL/LAT +247,583,005,95,lv,Latvia,371,Bite Latvija +247,583,001,31,lv,Latvia,371,Latvian Mobile Phone +247,583,009,159,lv,Latvia,371,SIA Camel Mobile +247,583,008,143,lv,Latvia,371,SIA IZZI +247,583,007,127,lv,Latvia,371,SIA Master Telecom +247,583,006,111,lv,Latvia,371,SIA Rigatta +247,583,002,47,lv,Latvia,371,Tele2 +247,583,003,63,lv,Latvia,371,TRIATEL/Telekom Baltija +415,1045,033,831,lb,Lebanon,961,Cellis +415,1045,032,815,lb,Lebanon,961,Cellis2 +415,1045,035,863,lb,Lebanon,961,Cellis3 +415,1045,034,847,lb,Lebanon,961,FTML Cellis +415,1045,039,927,lb,Lebanon,961,MIC2/LibanCell +415,1045,038,911,lb,Lebanon,961,MIC2/LibanCell2 +415,1045,037,895,lb,Lebanon,961,MIC2/LibanCell3 +415,1045,001,31,lb,Lebanon,961,MIC1 (Alfa) +415,1045,003,63,lb,Lebanon,961,Touch +415,1045,036,879,lb,Lebanon,961,MIC2/LibanCell +651,1617,002,47,ls,Lesotho,266,Econet/Ezi-cel +651,1617,001,31,ls,Lesotho,266,Vodacom Lesotho +618,1560,007,127,lr,Liberia,231,Celcom +618,1560,003,63,lr,Liberia,231,Celcom2 +618,1560,004,79,lr,Liberia,231,Comium BVI +618,1560,002,47,lr,Liberia,231,Libercell +618,1560,020,527,lr,Liberia,231,LibTelco +618,1560,001,31,lr,Liberia,231,Lonestar +606,1542,002,47,ly,Libya,218,Al-Madar +606,1542,001,31,ly,Libya,218,Al-Madar2 +606,1542,006,111,ly,Libya,218,Hatef +606,1542,000,15,ly,Libya,218,Libyana +606,1542,003,63,ly,Libya,218,Libyana2 +295,661,006,111,li,Liechtenstein,423,CUBIC (Liechtenstein +295,661,007,127,li,Liechtenstein,423,First Mobile AG +295,661,005,95,li,Liechtenstein,423,Mobilkom AG +295,661,002,47,li,Liechtenstein,423,Orange +295,661,001,31,li,Liechtenstein,423,Swisscom FL AG +295,661,077,1919,li,Liechtenstein,423,Alpmobile/Tele2 +246,582,002,47,lt,Lithuania,370,Bite +246,582,001,31,lt,Lithuania,370,Omnitel +246,582,003,63,lt,Lithuania,370,Tele2 +270,624,077,1919,lu,Luxembourg,352,Millicom Tango GSM +270,624,001,31,lu,Luxembourg,352,P+T LUXGSM +270,624,099,2463,lu,Luxembourg,352,VOXmobile S.A. +455,1109,004,79,mo,Macao China,853,C.T.M. TELEMOVEL+ +455,1109,001,31,mo,Macao China,853,C.T.M. TELEMOVEL+2 +455,1109,002,47,mo,Macao China,853,China Telecom +455,1109,005,95,mo,Macao China,853,Hutchison Telephone (Macau) Company Ltd +455,1109,003,63,mo,Macao China,853,Hutchison Telephone (Macau) Company Ltd-2 +455,1109,006,111,mo,Macao China,853,Smartone Mobile +455,1109,000,15,mo,Macao China,853,Smartone Mobile-2 +294,660,075,1887,mk,Macedonia,389,MTS/Cosmofone +294,660,002,47,mk,Macedonia,389,MTS/Cosmofone2 +294,660,001,31,mk,Macedonia,389,T-Mobile/Mobimak +294,660,003,63,mk,Macedonia,389,VIP Mobile +646,1606,001,31,mg,Madagascar,261,MADACOM +646,1606,002,47,mg,Madagascar,261,Orange/Soci +646,1606,003,63,mg,Madagascar,261,Sacel +646,1606,004,79,mg,Madagascar,261,Telma +650,1616,001,31,mw,Malawi,265,TNM/Telekom Network Ltd. +650,1616,010,271,mw,Malawi,265,Zain/Celtel ltd. +502,1282,001,31,my,Malaysia,60,Art900 +502,1282,151,337,my,Malaysia,60,Baraka Telecom Sdn Bhd +502,1282,013,319,my,Malaysia,60,CelCom +502,1282,019,415,my,Malaysia,60,CelCom2 +502,1282,016,367,my,Malaysia,60,Digi Telecommunications +502,1282,010,271,my,Malaysia,60,Digi Telecommunications-2 +502,1282,020,527,my,Malaysia,60,Electcoms Wireless Sdn Bhd +502,1282,012,303,my,Malaysia,60,Maxis +502,1282,017,383,my,Malaysia,60,Maxis2 +502,1282,011,287,my,Malaysia,60,MTX Utara +502,1282,153,339,my,Malaysia,60,Packet One Networks (Malaysia) Sdn Bhd +502,1282,155,341,my,Malaysia,60,Samata Communications Sdn Bhd +502,1282,154,340,my,Malaysia,60,Talk Focus Sdn Bhd +502,1282,018,399,my,Malaysia,60,U Mobile +502,1282,152,338,my,Malaysia,60,YES +472,1138,001,31,mv,Maldives,960,Dhiraagu/C&W +472,1138,002,47,mv,Maldives,960,Wataniya/WMOBILE +610,1552,001,31,ml,Mali,223,Malitel +610,1552,002,47,ml,Mali,223,Orange/IKATEL +278,632,021,543,mt,Malta,356,GO/Mobisle +278,632,077,1919,mt,Malta,356,Melita +278,632,001,31,mt,Malta,356,Vodafone +340,832,002,47,mq,Martinique (French Department of),,Outremer Telecom Martinique (only) (Martinique +340,832,012,303,mq,Martinique (French Department of),,United Telecommunications Services Caraibe SARL (UTS Caraibe Martinique Telephone Mobile) (Martinique +340,832,003,63,mq,Martinique (French Department of),,United Telecommunications Services Caraibe SARL (UTS Caraibe Martinique Telephone Mobile) (Martinique-2 +609,1545,002,47,mr,Mauritania,222,Chinguitel SA +609,1545,001,31,mr,Mauritania,222,Mattel +609,1545,010,271,mr,Mauritania,222,Mauritel +617,1559,010,271,mu,Mauritius,230,Emtel Ltd +617,1559,002,47,mu,Mauritius,230,Mahanagar Telephone +617,1559,003,63,mu,Mauritius,230,Mahanagar Telephone2 +617,1559,001,31,mu,Mauritius,230,Orange/Cellplus +334,820,000,15,mx,Mexico,52,Axtel +334,820,050,1295,mx,Mexico,52,IUSACell/UneFon +334,820,050,80,mx,Mexico,52,IUSACell/UneFon +334,820,040,64,mx,Mexico,52,IUSACell/UneFon2 +334,820,004,79,mx,Mexico,52,IUSACell/UneFon3 +334,820,003,63,mx,Mexico,52,Movistar/Pegaso +334,820,030,48,mx,Mexico,52,Movistar/Pegaso2 +334,820,001,31,mx,Mexico,52,NEXTEL +334,820,090,144,mx,Mexico,52,NEXTEL2 +334,820,010,16,mx,Mexico,52,NEXTEL3 +334,820,080,128,mx,Mexico,52,Operadora Unefon SA de CV +334,820,070,112,mx,Mexico,52,Operadora Unefon SA de CV2 +334,820,060,96,mx,Mexico,52,SAI PCS +334,820,000,15,mx,Mexico,52,SAI PCS-2 +334,820,002,47,mx,Mexico,52,TelCel/America Movil +334,820,020,32,mx,Mexico,52,TelCel/America Movil-2 +550,1360,001,31,fm,Micronesia,691,FSM Telecom +259,601,004,79,md,Moldova,373,Eventis Mobile +259,601,099,2463,md,Moldova,373,IDC/Unite +259,601,005,95,md,Moldova,373,IDC/Unite2 +259,601,003,63,md,Moldova,373,IDC/Unite3 +259,601,002,47,md,Moldova,373,Moldcell +259,601,001,31,md,Moldova,373,Orange/Voxtel +212,530,001,31,mc,Monaco,377,Dardafone LLC +212,530,010,271,mc,Monaco,377,Monaco Telecom +212,530,001,31,mc,Monaco,377,Monaco Telecom2 +212,530,001,31,mc,Monaco,377,Post and Telecommunications of Kosovo JSC (PTK) +428,1064,098,2447,mn,Mongolia,976,G-Mobile Corporation Ltd +428,1064,099,2463,mn,Mongolia,976,Mobicom +428,1064,000,15,mn,Mongolia,976,Skytel Co. Ltd +428,1064,088,2191,mn,Mongolia,976,Unitel +297,663,002,47,me,Montenegro,382,Monet/T-mobile +297,663,003,63,me,Montenegro,382,Mtel +297,663,001,31,me,Montenegro,382,Promonte GSM +354,852,860,2144,ms,Montserrat,1664,Cable & Wireless +604,1540,001,31,ma,Morocco,212,IAM/Itissallat +604,1540,002,47,ma,Morocco,212,INWI/WANA +604,1540,000,15,ma,Morocco,212,Medi Telecom +643,1603,001,31,mz,Mozambique,258,mCel +643,1603,003,63,mz,Mozambique,258,Movitel +643,1603,004,79,mz,Mozambique,258,Vodacom Sarl +649,1609,003,63,na,Namibia,264,Leo / Orascom +649,1609,001,31,na,Namibia,264,MTC +649,1609,002,47,na,Namibia,264,Switch/Nam. Telec. +429,1065,002,47,np,Nepal,977,Ncell +429,1065,001,31,np,Nepal,977,NT Mobile / Namaste +429,1065,004,79,np,Nepal,977,Smart Cell +204,516,014,335,nl,Netherlands,31,6GMOBILE BV +204,516,023,575,nl,Netherlands,31,Aspider Solutions +204,516,005,95,nl,Netherlands,31,Elephant Talk Communications Premium Rate Services Netherlands BV +204,516,017,383,nl,Netherlands,31,Intercity Mobile Communications BV +204,516,010,271,nl,Netherlands,31,KPN Telecom B.V. +204,516,008,143,nl,Netherlands,31,KPN Telecom B.V.2 +204,516,069,1695,nl,Netherlands,31,KPN Telecom B.V.3 +204,516,012,303,nl,Netherlands,31,KPN/Telfort +204,516,028,655,nl,Netherlands,31,Lancelot BV +204,516,009,159,nl,Netherlands,31,Lycamobile Ltd +204,516,006,111,nl,Netherlands,31,Mundio/Vectone Mobile +204,516,021,543,nl,Netherlands,31,NS Railinfrabeheer B.V. +204,516,024,591,nl,Netherlands,31,Private Mobility Nederland BV +204,516,098,2447,nl,Netherlands,31,T-Mobile B.V. +204,516,016,367,nl,Netherlands,31,T-Mobile B.V.2 +204,516,020,527,nl,Netherlands,31,Orange/T-mobile +204,516,002,47,nl,Netherlands,31,Tele2 +204,516,007,127,nl,Netherlands,31,Teleena Holding BV +204,516,068,1679,nl,Netherlands,31,Unify Mobile +204,516,018,399,nl,Netherlands,31,UPC Nederland BV +204,516,004,79,nl,Netherlands,31,Vodafone Libertel +204,516,003,63,nl,Netherlands,31,Voiceworks Mobile BV +204,516,015,351,nl,Netherlands,31,Ziggo BV +362,866,630,1584,an,Netherlands Antilles,599,Cingular Wireless +362,866,069,1695,an,Netherlands Antilles,599,DigiCell +362,866,051,1311,an,Netherlands Antilles,599,TELCELL GSM +362,866,091,2335,an,Netherlands Antilles,599,SETEL GSM +362,866,951,2385,an,Netherlands Antilles,599,UTS Wireless +546,1350,001,31,nc,New Caledonia,687,OPT Mobilis +530,1328,028,655,nz,New Zealand,64,2degrees +530,1328,005,95,nz,New Zealand,64,Telecom Mobile Ltd +530,1328,002,2,nz,New Zealand,64,NZ Telecom CDMA +530,1328,004,4,nz,New Zealand,64,Telstra +530,1328,024,591,nz,New Zealand,64,Two Degrees Mobile Ltd +530,1328,001,31,nz,New Zealand,64,Vodafone +530,1328,003,3,nz,New Zealand,64,Walker Wireless Ltd. +710,1808,021,543,ni,Nicaragua,505,Empresa Nicaraguense de Telecomunicaciones SA (ENITEL) +710,1808,030,783,ni,Nicaragua,505,Movistar +710,1808,073,1855,ni,Nicaragua,505,Claro +614,1556,003,63,ne,Niger,227,Etisalat/TeleCel +614,1556,004,79,ne,Niger,227,Orange/Sahelc. +614,1556,001,31,ne,Niger,227,Orange/Sahelc.2 +614,1556,002,47,ne,Niger,227,Zain/CelTel +621,1569,020,527,ng,Nigeria,234,Airtel/ZAIN/Econet +621,1569,060,1551,ng,Nigeria,234,ETISALAT +621,1569,050,1295,ng,Nigeria,234,Glo Mobile +621,1569,040,1039,ng,Nigeria,234,M-Tel/Nigeria Telecom. Ltd. +621,1569,030,783,ng,Nigeria,234,MTN +621,1569,099,2463,ng,Nigeria,234,Starcomms +621,1569,025,607,ng,Nigeria,234,Visafone +621,1569,001,31,ng,Nigeria,234,Visafone2 +555,1365,001,31,nu,Niue,,Niue Telecom +242,578,009,159,no,Norway,47,Com4 AS +242,578,020,527,no,Norway,47,Jernbaneverket (GSM-R) +242,578,021,543,no,Norway,47,Jernbaneverket (GSM-R)-2 +242,578,023,575,no,Norway,47,Lycamobile Ltd +242,578,002,47,no,Norway,47,Netcom +242,578,022,559,no,Norway,47,Network Norway AS +242,578,005,95,no,Norway,47,Network Norway AS-2 +242,578,006,6,no,Norway,47,ICE Nordisk Mobiltelefon AS +242,578,008,143,no,Norway,47,TDC Mobil A/S +242,578,004,79,no,Norway,47,Tele2 +242,578,012,303,no,Norway,47,Telenor +242,578,001,31,no,Norway,47,Telenor2 +242,578,003,63,no,Norway,47,Teletopia +242,578,007,127,no,Norway,47,Ventelo AS +422,1058,003,63,om,Oman,968,Nawras +422,1058,002,47,om,Oman,968,Oman Mobile/GTO +410,1040,008,143,pk,Pakistan,92,Instaphone +410,1040,001,31,pk,Pakistan,92,Mobilink +410,1040,006,111,pk,Pakistan,92,Telenor +410,1040,003,63,pk,Pakistan,92,UFONE/PAKTel +410,1040,007,127,pk,Pakistan,92,Warid Telecom +410,1040,004,79,pk,Pakistan,92,ZONG/CMPak +552,1362,080,2063,pw,Palau (Republic of),,Palau Mobile Corp. (PMC) (Palau +552,1362,001,31,pw,Palau (Republic of),,Palau National Communications Corp. (PNCC) (Palau +425,1061,005,95,ps,Palestinian Territory,970,Jawwal +425,1061,006,111,ps,Palestinian Territory,970,Wataniya Mobile +714,1812,001,31,pa,Panama,507,Cable & Wireless S.A. +714,1812,003,63,pa,Panama,507,Claro +714,1812,004,79,pa,Panama,507,Digicel +714,1812,020,32,pa,Panama,507,Movistar +714,1812,002,47,pa,Panama,507,Movistar2 +537,1335,003,63,pg,Papua New Guinea,675,Digicel +537,1335,002,47,pg,Papua New Guinea,675,GreenCom PNG Ltd +537,1335,001,31,pg,Papua New Guinea,675,Pacific Mobile +744,1860,002,47,py,Paraguay,595,Claro/Hutchison +744,1860,003,63,py,Paraguay,595,Compa +744,1860,001,31,py,Paraguay,595,Hola/VOX +744,1860,005,5,py,Paraguay,595,TIM/Nucleo/Personal +744,1860,004,79,py,Paraguay,595,Tigo/Telecel +716,1814,020,527,pe,Peru,51,Claro /Amer.Mov./TIM +716,1814,010,271,pe,Peru,51,Claro /Amer.Mov./TIM-2 +716,1814,002,47,pe,Peru,51,GlobalStar +716,1814,001,31,pe,Peru,51,GlobalStar2 +716,1814,006,111,pe,Peru,51,Movistar +716,1814,007,127,pe,Peru,51,Nextel +515,1301,000,15,ph,Philippines,63,Fix Line +515,1301,001,31,ph,Philippines,63,Globe Telecom +515,1301,002,47,ph,Philippines,63,Globe Telecom2 +515,1301,088,2191,ph,Philippines,63,Next Mobile +515,1301,018,399,ph,Philippines,63,RED Mobile/Cure +515,1301,003,63,ph,Philippines,63,Smart +515,1301,005,95,ph,Philippines,63,SUN/Digitel +260,608,017,383,pl,Poland,48,Aero2 SP. +260,608,018,399,pl,Poland,48,AMD Telecom. +260,608,038,911,pl,Poland,48,CallFreedom Sp. z o.o. +260,608,012,303,pl,Poland,48,Cyfrowy POLSAT S.A. +260,608,008,143,pl,Poland,48,e-Telko +260,608,009,159,pl,Poland,48,Lycamobile +260,608,016,367,pl,Poland,48,Mobyland +260,608,036,879,pl,Poland,48,Mundio Mobile Sp. z o.o. +260,608,007,127,pl,Poland,48,Play/P4 +260,608,011,287,pl,Poland,48,NORDISK Polska +260,608,005,95,pl,Poland,48,Orange/IDEA/Centertel +260,608,003,63,pl,Poland,48,Orange/IDEA/Centertel2 +260,608,035,863,pl,Poland,48,PKP Polskie Linie Kolejowe S.A. +260,608,098,2447,pl,Poland,48,Play/P4 +260,608,006,111,pl,Poland,48,Play/P4-2 +260,608,001,31,pl,Poland,48,Polkomtel/Plus +260,608,010,271,pl,Poland,48,Sferia +260,608,014,335,pl,Poland,48,Sferia2 +260,608,013,319,pl,Poland,48,Sferia3 +260,608,034,847,pl,Poland,48,T-Mobile/ERA +260,608,002,47,pl,Poland,48,T-Mobile/ERA-2 +260,608,015,351,pl,Poland,48,Tele2 +260,608,004,79,pl,Poland,48,Tele2 +268,616,004,79,pt,Portugal,351,CTT - Correios de Portugal SA +268,616,003,63,pt,Portugal,351,Optimus +268,616,007,127,pt,Portugal,351,Optimus2 +268,616,006,111,pt,Portugal,351,TMN +268,616,001,31,pt,Portugal,351,Vodafone +330,816,011,287,pr,Puerto Rico,,Puerto Rico Telephone Company Inc. (PRTC) +427,1063,001,31,qa,Qatar,974,Qtel +427,1063,002,47,qa,Qatar,974,Vodafone +647,1607,000,15,re,Reunion,262,Orange +647,1607,002,47,re,Reunion,262,Outremer Telecom +647,1607,010,271,re,Reunion,262,SFR +226,550,003,63,ro,Romania,40,Cosmote +226,550,011,287,ro,Romania,40,Enigma Systems +226,550,010,271,ro,Romania,40,Orange +226,550,005,95,ro,Romania,40,RCS&RDS Digi Mobile +226,550,002,47,ro,Romania,40,Romtelecom SA +226,550,006,111,ro,Romania,40,Telemobil/Zapp +226,550,001,31,ro,Romania,40,Vodafone +226,550,004,79,ro,Romania,40,Telemobil/Zapp-2 +250,592,012,303,ru,Russian Federation,79,Baykal Westcom +250,592,028,655,ru,Russian Federation,79,Bee Line GSM +250,592,010,271,ru,Russian Federation,79,DTC/Don Telecom +250,592,020,527,ru,Russian Federation,79,JSC Rostov Cellular Communications +250,592,013,319,ru,Russian Federation,79,Kuban GSM +250,592,035,863,ru,Russian Federation,79,LLC Ekaterinburg-2000 +250,592,020,527,ru,Russian Federation,79,LLC Personal Communication Systems in the Region +250,592,002,47,ru,Russian Federation,79,Megafon +250,592,001,31,ru,Russian Federation,79,MTS +250,592,003,63,ru,Russian Federation,79,NCC +250,592,016,367,ru,Russian Federation,79,NTC +250,592,019,415,ru,Russian Federation,79,OJSC Altaysvyaz +250,592,099,2463,ru,Russian Federation,79,OJSC Vimpel-Communications (VimpelCom) +250,592,011,287,ru,Russian Federation,79,Orensot +250,592,092,2351,ru,Russian Federation,79,Printelefone +250,592,004,79,ru,Russian Federation,79,Sibchallenge +250,592,044,1103,ru,Russian Federation,79,StavTelesot +250,592,020,527,ru,Russian Federation,79,Tele2/ECC/Volgogr. +250,592,093,2367,ru,Russian Federation,79,Telecom XXL +250,592,039,927,ru,Russian Federation,79,U-Tel/Ermak RMS +250,592,017,383,ru,Russian Federation,79,U-Tel/Ermak RMS-2 +250,592,039,927,ru,Russian Federation,79,UralTel +250,592,017,383,ru,Russian Federation,79,UralTel2 +250,592,005,95,ru,Russian Federation,79,Yenisey Telecom +250,592,015,351,ru,Russian Federation,79,ZAO SMARTS +250,592,007,127,ru,Russian Federation,79,ZAO SMARTS-2 +635,1589,014,335,rw,Rwanda,250,Airtel Rwanda Ltd +635,1589,010,271,rw,Rwanda,250,MTN/Rwandacell +635,1589,013,319,rw,Rwanda,250,TIGO +356,854,110,272,kn,Saint Kitts and Nevis,1869,Cable & Wireless +356,854,050,1295,kn,Saint Kitts and Nevis,1869,Digicel +356,854,070,1807,kn,Saint Kitts and Nevis,1869,UTS Cariglobe +358,856,110,272,lc,Saint Lucia,1758,Cable & Wireless +358,856,030,783,lc,Saint Lucia,1758,Cingular Wireless +358,856,050,1295,lc,Saint Lucia,1758,Digicel (St Lucia) Limited +549,1353,027,639,ws,Samoa,685,Samoatel Mobile +549,1353,001,31,ws,Samoa,685,Telecom Samoa Cellular Ltd. +292,658,001,31,sm,San Marino,378,Prima Telecom +626,1574,001,31,st,Sao Tome & Principe,239,CSTmovel +901,2305,014,335,n/a,Satellite Networks,870,AeroMobile +901,2305,011,287,n/a,Satellite Networks,870,InMarSAT +901,2305,012,303,n/a,Satellite Networks,870,Maritime Communications Partner AS +901,2305,005,95,n/a,Satellite Networks,870,Thuraya Satellite +420,1056,007,7,sa,Saudi Arabia,966,Zain +420,1056,003,63,sa,Saudi Arabia,966,Etihad/Etisalat/Mobily +420,1056,001,31,sa,Saudi Arabia,966,STC/Al Jawal +420,1056,004,79,sa,Saudi Arabia,966,Zain +608,1544,003,63,sn,Senegal,221,Expresso/Sudatel +608,1544,001,31,sn,Senegal,221,Orange/Sonatel +608,1544,002,47,sn,Senegal,221,Sentel GSM +220,544,003,63,rs,Serbia,381,MTS/Telekom Srbija +220,544,002,47,rs,Serbia,381,Telenor/Mobtel +220,544,001,1,rs,Serbia,381,Telenor/Mobtel2 +220,544,005,95,rs,Serbia,381,VIP Mobile +633,1587,010,271,sc,Seychelles,248,Airtel +633,1587,001,31,sc,Seychelles,248,C&W +633,1587,002,47,sc,Seychelles,248,Smartcom +619,1561,003,3,sl,Sierra Leone,232,Africel +619,1561,005,5,sl,Sierra Leone,232,Africel2 +619,1561,001,1,sl,Sierra Leone,232,Zain/Celtel +619,1561,004,4,sl,Sierra Leone,232,Comium +619,1561,002,2,sl,Sierra Leone,232,Tigo/Millicom +619,1561,025,607,sl,Sierra Leone,232,Mobitel +525,1317,012,303,sg,Singapore,65,GRID Communications Pte Ltd +525,1317,003,63,sg,Singapore,65,MobileOne Ltd +525,1317,001,31,sg,Singapore,65,Singtel +525,1317,007,127,sg,Singapore,65,Singtel2 +525,1317,002,47,sg,Singapore,65,Singtel3 +525,1317,006,111,sg,Singapore,65,Starhub +525,1317,005,95,sg,Singapore,65,Starhub2 +362,866,051,1311,sx,Sint Maarten (Dutch part),,TelCell NV (Sint Maarten +362,866,091,2335,sx,Sint Maarten (Dutch part),,UTS St. Maarten (Sint Maarten +231,561,006,111,sk,Slovakia,421,O2 +231,561,001,31,sk,Slovakia,421,Orange +231,561,005,95,sk,Slovakia,421,Orange2 +231,561,015,351,sk,Slovakia,421,Orange +231,561,002,47,sk,Slovakia,421,T-Mobile +231,561,004,79,sk,Slovakia,421,T-Mobile3 +231,561,099,2463,sk,Slovakia,421,Zeleznice Slovenskej republiky (ZSR) +293,659,041,1055,si,Slovenia,386,Dukagjini Telecommunications Sh.P.K. +293,659,041,1055,si,Slovenia,386,Ipko Telecommunications d. o. o. +293,659,041,1055,si,Slovenia,386,Mobitel +293,659,040,1039,si,Slovenia,386,SI.Mobil +293,659,010,271,si,Slovenia,386,Slovenske zeleznice d.o.o. +293,659,064,1615,si,Slovenia,386,T-2 d.o.o. +293,659,070,1807,si,Slovenia,386,TusMobil/VEGA +540,1344,002,47,sb,Solomon Islands,677,bemobile +540,1344,010,271,sb,Solomon Islands,677,BREEZE +540,1344,001,31,sb,Solomon Islands,677,BREEZE2 +637,1591,030,783,so,Somalia,252,Golis +637,1591,019,415,so,Somalia,252,HorTel +637,1591,060,1551,so,Somalia,252,Nationlink +637,1591,010,271,so,Somalia,252,Nationlink2 +637,1591,004,4,so,Somalia,252,Somafone +637,1591,082,2095,so,Somalia,252,Telcom Mobile Somalia +637,1591,001,31,so,Somalia,252,Telesom +655,1621,002,47,za,South Africa,27,8.ta +655,1621,021,543,za,South Africa,27,Cape Town Metropolitan +655,1621,007,127,za,South Africa,27,Cell C +655,1621,012,303,za,South Africa,27,MTN +655,1621,010,271,za,South Africa,27,MTN2 +655,1621,006,111,za,South Africa,27,Sentech +655,1621,001,31,za,South Africa,27,Vodacom +655,1621,019,415,za,South Africa,27,Wireless Business Solutions (Pty) Ltd +659,1625,003,63,ss,South Sudan (Republic of),,Gemtel Ltd (South Sudan +659,1625,002,47,ss,South Sudan (Republic of),,MTN South Sudan (South Sudan +659,1625,004,79,ss,South Sudan (Republic of),,Network of The World Ltd (NOW) (South Sudan +659,1625,006,111,ss,South Sudan (Republic of),,Zain South Sudan (South Sudan +214,532,023,575,es,Spain,34,Lycamobile SL +214,532,022,559,es,Spain,34,Movistar2 +214,532,015,351,es,Spain,34,BT Espana Compania de Servicios Globales de Telecomunicaciones SAU +214,532,018,399,es,Spain,34,Cableuropa SAU (ONO) +214,532,008,143,es,Spain,34,Euskaltel SA +214,532,020,527,es,Spain,34,fonYou Wireless SL +214,532,021,543,es,Spain,34,Jazz Telecom SAU +214,532,026,623,es,Spain,34,Lleida +214,532,025,607,es,Spain,34,Lycamobile SL +214,532,007,127,es,Spain,34,Movistar +214,532,005,95,es,Spain,34,Movistar (resellers) +214,532,009,159,es,Spain,34,Orange (resellers) +214,532,003,63,es,Spain,34,Orange +214,532,011,287,es,Spain,34,Orange2 +214,532,017,383,es,Spain,34,R Cable y Telecomunicaciones Galicia SA +214,532,019,415,es,Spain,34,Simyo/KPN +214,532,016,367,es,Spain,34,Telecable de Asturias SA +214,532,027,639,es,Spain,34,Truphone +214,532,001,31,es,Spain,34,Vodafone +214,532,006,111,es,Spain,34,Vodafone Enabler Espana SL +214,532,004,79,es,Spain,34,Yoigo +413,1043,005,95,lk,Sri Lanka,94,Bharti Airtel +413,1043,003,63,lk,Sri Lanka,94,Etisalat/Tigo +413,1043,008,143,lk,Sri Lanka,94,H3G Hutchison +413,1043,001,31,lk,Sri Lanka,94,Mobitel Ltd. +413,1043,002,47,lk,Sri Lanka,94,MTN/Dialog +308,776,001,31,pm,St. Pierre & Miquelon,508,Ameris +360,864,110,272,vc,St. Vincent & Gren.,1784,C & W +360,864,010,271,vc,St. Vincent & Gren.,1784,Cingular +360,864,100,256,vc,St. Vincent & Gren.,1784,Cingular-2 +360,864,050,80,vc,St. Vincent & Gren.,1784,Digicel +360,864,070,1807,vc,St. Vincent & Gren.,1784,Digicel +634,1588,000,15,sd,Sudan,249,Canar Telecom +634,1588,022,559,sd,Sudan,249,MTN +634,1588,002,47,sd,Sudan,249,MTN +634,1588,015,351,sd,Sudan,249,Sudani One +634,1588,007,127,sd,Sudan,249,Sudani One-2 +634,1588,005,95,sd,Sudan,249,Vivacell +634,1588,008,143,sd,Sudan,249,Vivacell +634,1588,001,31,sd,Sudan,249,ZAIN/Mobitel +634,1588,006,111,sd,Sudan,249,ZAIN/Mobitel-2 +746,1862,003,63,sr,Suriname,597,Digicel +746,1862,002,47,sr,Suriname,597,Telecommunicatiebedrijf Suriname (TELESUR) +746,1862,001,1,sr,Suriname,597,Telesur +746,1862,004,79,sr,Suriname,597,UNIQA +653,1619,010,271,sz,Swaziland,268,Swazi MTN +653,1619,001,31,sz,Swaziland,268,SwaziTelecom +240,576,035,863,se,Sweden,46,42 Telecom AB +240,576,016,367,se,Sweden,46,42 Telecom AB-2 +240,576,026,623,se,Sweden,46,Beepsend +240,576,000,15,se,Sweden,46,Compatel +240,576,028,655,se,Sweden,46,CoolTEL Aps +240,576,025,607,se,Sweden,46,Digitel Mobile Srl +240,576,022,559,se,Sweden,46,Eu Tel AB +240,576,027,639,se,Sweden,46,Fogg Mobile AB +240,576,018,399,se,Sweden,46,Generic Mobile Systems Sweden AB +240,576,017,383,se,Sweden,46,Gotalandsnatet AB +240,576,002,47,se,Sweden,46,H3G Access AB +240,576,004,79,se,Sweden,46,H3G Access AB-2 +240,576,036,879,se,Sweden,46,ID Mobile +240,576,023,575,se,Sweden,46,Infobip Ltd. +240,576,011,287,se,Sweden,46,Lindholmen Science Park AB +240,576,012,303,se,Sweden,46,Lycamobile Ltd +240,576,029,671,se,Sweden,46,Mercury International Carrier Services +240,576,003,63,se,Sweden,46,Orange +240,576,010,271,se,Sweden,46,Spring Mobil AB +240,576,014,335,se,Sweden,46,TDC Sverige AB +240,576,007,127,se,Sweden,46,Tele2 Sverige AB +240,576,005,95,se,Sweden,46,Tele2 Sverige AB +240,576,024,591,se,Sweden,46,Tele2 Sverige AB-2 +240,576,024,591,se,Sweden,46,Telenor (Vodafone) +240,576,008,143,se,Sweden,46,Telenor (Vodafone)-2 +240,576,004,79,se,Sweden,46,Telenor (Vodafone)-3 +240,576,006,111,se,Sweden,46,Telenor (Vodafone)-4 +240,576,009,159,se,Sweden,46,Telenor Mobile Sverige AS +240,576,005,95,se,Sweden,46,Telia Mobile +240,576,001,31,se,Sweden,46,Telia Mobile-2 +240,576,000,15,se,Sweden,46,EUTel +240,576,008,143,se,Sweden,46,Timepiece Servicos De Consultoria LDA (Universal Telecom) +240,576,013,319,se,Sweden,46,Ventelo Sverige AB +240,576,020,527,se,Sweden,46,Wireless Maingate AB +240,576,015,351,se,Sweden,46,Wireless Maingate Nordic AB +228,552,051,1311,ch,Switzerland,41,BebbiCell AG +228,552,009,159,ch,Switzerland,41,Comfone AG +228,552,005,95,ch,Switzerland,41,Comfone AG +228,552,007,127,ch,Switzerland,41,TDC Sunrise +228,552,054,1359,ch,Switzerland,41,Lycamobile AG +228,552,052,1327,ch,Switzerland,41,Mundio Mobile AG +228,552,003,63,ch,Switzerland,41,Orange +228,552,001,31,ch,Switzerland,41,Swisscom +228,552,012,303,ch,Switzerland,41,TDC Sunrise2 +228,552,002,47,ch,Switzerland,41,TDC Sunrise3 +228,552,008,143,ch,Switzerland,41,TDC Sunrise4 +228,552,053,1343,ch,Switzerland,41,upc cablecom GmbH +417,1047,002,47,sy,Syrian Arab Republic,963,MTN/Spacetel +417,1047,009,159,sy,Syrian Arab Republic,963,Syriatel Holdings +417,1047,001,31,sy,Syrian Arab Republic,963,Syriatel Holdings2 +466,1126,068,1679,tw,Taiwan,886,ACeS Taiwan - ACeS Taiwan Telecommunications Co Ltd +466,1126,005,95,tw,Taiwan,886,Asia Pacific Telecom Co. Ltd (APT) +466,1126,011,287,tw,Taiwan,886,Chunghwa Telecom LDM +466,1126,092,2351,tw,Taiwan,886,Chunghwa Telecom LDM-2 +466,1126,002,47,tw,Taiwan,886,Far EasTone +466,1126,001,31,tw,Taiwan,886,Far EasTone2 +466,1126,007,127,tw,Taiwan,886,Far EasTone3 +466,1126,006,111,tw,Taiwan,886,Far EasTone4 +466,1126,003,63,tw,Taiwan,886,Far EasTone6 +466,1126,010,271,tw,Taiwan,886,Global Mobile Corp. +466,1126,056,1391,tw,Taiwan,886,International Telecom Co. Ltd (FITEL) +466,1126,088,2191,tw,Taiwan,886,KG Telecom +466,1126,099,2463,tw,Taiwan,886,TransAsia +466,1126,097,2431,tw,Taiwan,886,Taiwan Cellular +466,1126,093,2367,tw,Taiwan,886,Mobitai +466,1126,089,2207,tw,Taiwan,886,VIBO +466,1126,009,159,tw,Taiwan,886,VMAX Telecom Co. Ltd +436,1078,004,79,tj,Tajikistan,992,Babilon-M +436,1078,005,95,tj,Tajikistan,992,Bee Line +436,1078,002,47,tj,Tajikistan,992,CJSC Indigo Tajikistan +436,1078,012,303,tj,Tajikistan,992,Tcell/JC Somoncom +436,1078,003,63,tj,Tajikistan,992,MLT/TT mobile +436,1078,001,31,tj,Tajikistan,992,Tcell/JC Somoncom +640,1600,008,143,tz,Tanzania,255,Benson Informatics Ltd +640,1600,006,111,tz,Tanzania,255,Dovetel (T) Ltd +640,1600,009,159,tz,Tanzania,255,ExcellentCom (T) Ltd +640,1600,011,287,tz,Tanzania,255,Smile Communications Tanzania Ltd +640,1600,007,127,tz,Tanzania,255,Tanzania Telecommunications Company Ltd (TTCL) +640,1600,002,47,tz,Tanzania,255,TIGO/MIC +640,1600,001,1,tz,Tanzania,255,Tri Telecomm. Ltd. +640,1600,004,79,tz,Tanzania,255,Vodacom Ltd +640,1600,005,95,tz,Tanzania,255,ZAIN/Celtel +640,1600,003,63,tz,Tanzania,255,Zantel/Zanzibar Telecom +520,1312,020,527,th,Thailand,66,ACeS Thailand - ACeS Regional Services Co Ltd +520,1312,015,351,th,Thailand,66,ACT Mobile +520,1312,003,63,th,Thailand,66,Advanced Wireless Networks/AWN +520,1312,001,31,th,Thailand,66,AIS/Advanced Info Service +520,1312,023,575,th,Thailand,66,Digital Phone Co. +520,1312,000,15,th,Thailand,66,Hutch/CAT CDMA +520,1312,018,399,th,Thailand,66,Total Access (DTAC) +520,1312,005,95,th,Thailand,66,Total Access (DTAC) +520,1312,004,79,th,Thailand,66,True Move/Orange +520,1312,099,2463,th,Thailand,66,True Move/Orange +514,1300,001,31,tl,Timor-Leste,670,Telin/ Telkomcel +514,1300,002,47,tl,Timor-Leste,670,Timor Telecom +615,1557,002,2,tg,Togo,228,Telecel/MOOV +615,1557,003,63,tg,Togo,228,Telecel/MOOV-2 +615,1557,001,31,tg,Togo,228,Togo Telecom/TogoCELL +539,1337,043,1087,to,Tonga,676,Shoreline Communication +539,1337,001,1,to,Tonga,676,Tonga Communications +374,884,129,297,tt,Trinidad and Tobago,1868,Bmobile/TSTT +374,884,130,304,tt,Trinidad and Tobago,1868,Digicel +374,884,140,320,tt,Trinidad and Tobago,1868,LaqTel Ltd. +605,1541,001,31,tn,Tunisia,216,Orange +605,1541,003,63,tn,Tunisia,216,Orascom Telecom +605,1541,002,47,tn,Tunisia,216,Tunisie Telecom +286,646,004,79,tr,Turkey,90,AVEA/Aria +286,646,003,63,tr,Turkey,90,AVEA/Aria-2 +286,646,001,31,tr,Turkey,90,Turkcell +286,646,002,47,tr,Turkey,90,Vodafone-Telsim +438,1080,001,31,tm,Turkmenistan,993,Barash Communication +438,1080,002,47,tm,Turkmenistan,993,TM-Cell +376,886,350,848,tc,Turks and Caicos Islands,,Cable & Wireless (TCI) Ltd +376,886,050,80,tc,Turks and Caicos Islands,,Digicel TCI Ltd +376,886,352,850,tc,Turks and Caicos Islands,,IslandCom Communications Ltd. +553,1363,001,31,tv,Tuvalu,,Tuvalu Telecommunication Corporation (TTC) +641,1601,001,31,ug,Uganda,256,Celtel +641,1601,066,1647,ug,Uganda,256,i-Tel Ltd +641,1601,030,783,ug,Uganda,256,K2 Telecom Ltd +641,1601,010,271,ug,Uganda,256,MTN Ltd. +641,1601,014,335,ug,Uganda,256,Orange +641,1601,033,831,ug,Uganda,256,Smile Communications Uganda Ltd +641,1601,018,399,ug,Uganda,256,Suretelecom Uganda Ltd +641,1601,011,287,ug,Uganda,256,Uganda Telecom Ltd. +641,1601,022,559,ug,Uganda,256,Airtel/Warid +255,597,006,111,ua,Ukraine,380,Astelit/LIFE +255,597,005,95,ua,Ukraine,380,Golden Telecom +255,597,039,927,ua,Ukraine,380,Golden Telecom +255,597,004,79,ua,Ukraine,380,Intertelecom Ltd (IT) +255,597,067,1663,ua,Ukraine,380,KyivStar +255,597,003,63,ua,Ukraine,380,KyivStar2 +255,597,021,543,ua,Ukraine,380,Telesystems Of Ukraine CJSC (TSU) +255,597,007,127,ua,Ukraine,380,TriMob LLC +255,597,050,1295,ua,Ukraine,380,UMC/MTS +255,597,002,47,ua,Ukraine,380,Beeline +255,597,001,31,ua,Ukraine,380,UMC/MTS +255,597,068,1679,ua,Ukraine,380,Beeline +424,1060,003,63,ae,United Arab Emirates,971,DU +431,1073,002,47,ae,United Arab Emirates,971,Etisalat +430,1072,002,47,ae,United Arab Emirates,971,Etisalat-2 +424,1060,002,47,ae,United Arab Emirates,971,Etisalat-3 +234,564,003,63,gb,United Kingdom,44,Airtel/Vodafone +234,564,076,1903,gb,United Kingdom,44,BT Group +234,564,077,1919,gb,United Kingdom,44,BT Group2 +234,564,007,127,gb,United Kingdom,44,Cable and Wireless +234,564,092,2351,gb,United Kingdom,44,Cable and Wireless-2 +234,564,036,879,gb,United Kingdom,44,Calbe and Wireless Isle of Man +234,564,018,399,gb,United Kingdom,44,Cloud9/wire9 Tel. +235,565,002,47,gb,United Kingdom,44,Everyth. Ev.wh. +234,564,017,383,gb,United Kingdom,44,FlexTel +234,564,055,1375,gb,United Kingdom,44,Guernsey Telecoms +234,564,014,335,gb,United Kingdom,44,HaySystems +234,564,094,2383,gb,United Kingdom,44,Hutchinson 3G +234,564,020,527,gb,United Kingdom,44,Hutchinson 3G-2 +234,564,075,1887,gb,United Kingdom,44,Inquam Telecom Ltd +234,564,050,1295,gb,United Kingdom,44,Jersey Telecom +234,564,035,863,gb,United Kingdom,44,JSC Ingenicum +234,564,026,623,gb,United Kingdom,44,Lycamobile +234,564,058,1423,gb,United Kingdom,44,Manx Telecom +234,564,001,31,gb,United Kingdom,44,Mapesbury C. Ltd +234,564,028,655,gb,United Kingdom,44,Marthon Telecom +234,564,010,271,gb,United Kingdom,44,O2 Ltd. +234,564,002,47,gb,United Kingdom,44,O2 Ltd. +234,564,011,287,gb,United Kingdom,44,O2 Ltd. +234,564,008,143,gb,United Kingdom,44,OnePhone +234,564,016,367,gb,United Kingdom,44,Opal Telecom +234,564,034,847,gb,United Kingdom,44,Everyth. Ev.wh./Orange +234,564,033,831,gb,United Kingdom,44,Everyth. Ev.wh./Orange-2 +234,564,019,415,gb,United Kingdom,44,PMN/Teleware +234,564,012,303,gb,United Kingdom,44,Railtrack Plc +234,564,022,559,gb,United Kingdom,44,Routotelecom +234,564,024,591,gb,United Kingdom,44,Stour Marine +234,564,037,895,gb,United Kingdom,44,Synectiv Ltd. +234,564,031,799,gb,United Kingdom,44,Everyth. Ev.wh./T-Mobile +234,564,030,783,gb,United Kingdom,44,Everyth. Ev.wh./T-Mobile-2 +234,564,032,815,gb,United Kingdom,44,Everyth. Ev.wh./T-Mobile-3 +234,564,027,639,gb,United Kingdom,44,Vodafone +234,564,009,159,gb,United Kingdom,44,Tismi +234,564,025,607,gb,United Kingdom,44,Truphone +234,564,051,1311,gb,United Kingdom,44,Jersey Telecom +234,564,023,575,gb,United Kingdom,44,Vectofone Mobile Wifi +234,564,015,351,gb,United Kingdom,44,Vodafone +234,564,091,2335,gb,United Kingdom,44,Vodafone +234,564,078,1935,gb,United Kingdom,44,Wave Telecom Ltd +310,784,050,80,us,United States,1, +310,784,880,2176,us,United States,1, +310,784,850,2128,us,United States,1,Aeris Comm. Inc. +310,784,640,1600,us,United States,1, +310,784,510,1296,us,United States,1,Airtel Wireless LLC +310,784,190,400,us,United States,1,Unknown +312,786,090,144,us,United States,1,Allied Wireless Communications Corporation +311,785,130,304,us,United States,1, +311,785,030,48,us,United States,1,Americell PA3 LP +310,784,710,1808,us,United States,1,Arctic Slope Telephone Association Cooperative Inc. +310,784,380,896,us,United States,1,AT&T Wireless Inc. +310,784,170,368,us,United States,1,AT&T Wireless Inc.2 +310,784,150,336,us,United States,1,AT&T Wireless Inc.3 +310,784,680,1664,us,United States,1,AT&T Wireless Inc.4 +310,784,070,112,us,United States,1,AT&T Wireless Inc.5 +310,784,560,1376,us,United States,1,AT&T Wireless Inc.6 +310,784,410,1040,us,United States,1,AT&T Wireless Inc.7 +310,784,980,2432,us,United States,1,AT&T Wireless Inc.8 +311,785,440,1088,us,United States,1,Bluegrass Wireless LLC +311,785,810,2064,us,United States,1,Bluegrass Wireless LLC-2 +311,785,800,2048,us,United States,1,Bluegrass Wireless LLC-3 +310,784,900,2304,us,United States,1,Cable & Communications Corp. +311,785,590,1424,us,United States,1,California RSA No. 3 Limited Partnership +311,785,500,1280,us,United States,1,Cambridge Telephone Company Inc. +310,784,830,2096,us,United States,1,Caprock Cellular Ltd. +311,785,272,626,us,United States,1,Verizon Wireless +311,785,288,648,us,United States,1,Verizon Wireless2 +311,785,277,631,us,United States,1,Verizon Wireless3 +311,785,482,1154,us,United States,1,Verizon Wireless4 +310,784,590,1424,us,United States,1,Verizon Wireless5 +311,785,282,642,us,United States,1,Verizon Wireless6 +311,785,487,1159,us,United States,1,Verizon Wireless7 +311,785,271,625,us,United States,1,Verizon Wireless8 +311,785,287,647,us,United States,1,Verizon Wireless9 +311,785,276,630,us,United States,1,Verizon Wireless10 +311,785,481,1153,us,United States,1,Verizon Wireless11 +310,784,013,19,us,United States,1,Verizon Wireless12 +311,785,281,641,us,United States,1,Verizon Wireless13 +311,785,486,1158,us,United States,1,Verizon Wireless14 +311,785,270,624,us,United States,1,Verizon Wireless15 +311,785,286,646,us,United States,1,Verizon Wireless16 +311,785,275,629,us,United States,1,Verizon Wireless17 +311,785,480,1152,us,United States,1,Verizon Wireless18 +310,784,012,18,us,United States,1,Verizon Wireless19 +311,785,280,640,us,United States,1,Verizon Wireless20 +311,785,485,1157,us,United States,1,Verizon Wireless21 +311,785,110,272,us,United States,1,Verizon Wireless22 +311,785,285,645,us,United States,1,Verizon Wireless23 +311,785,274,628,us,United States,1,Verizon Wireless24 +311,785,390,912,us,United States,1,Verizon Wireless25 +310,784,010,16,us,United States,1,Verizon Wireless26 +311,785,279,633,us,United States,1,Verizon Wireless27 +311,785,484,1156,us,United States,1,Verizon Wireless28 +310,784,910,2320,us,United States,1,Verizon Wireless29 +311,785,284,644,us,United States,1,Verizon Wireless30 +311,785,489,1161,us,United States,1,Verizon Wireless31 +311,785,273,627,us,United States,1,Verizon Wireless32 +311,785,289,649,us,United States,1,Verizon Wireless33 +310,784,004,4,us,United States,1,Verizon Wireless34 +311,785,278,632,us,United States,1,Verizon Wireless35 +311,785,483,1155,us,United States,1,Verizon Wireless36 +310,784,890,2192,us,United States,1,Verizon Wireless37 +311,785,283,643,us,United States,1,Verizon Wireless38 +311,785,488,1160,us,United States,1,Verizon Wireless39 +312,786,280,640,us,United States,1,Cellular Network Partnership LLC +312,786,270,624,us,United States,1,Cellular Network Partnership LLC-2 +310,784,360,864,us,United States,1,Cellular Network Partnership LLC-3 +311,785,190,400,us,United States,1, +310,784,230,560,us,United States,1,Cellular South Licenses Inc. +310,784,030,48,us,United States,1, +311,785,120,288,us,United States,1,Choice Phone LLC +310,784,480,1152,us,United States,1,Choice Phone LLC-2 +310,784,630,1584,us,United States,1, +310,784,420,1056,us,United States,1,Cincinnati Bell Wireless LLC +310,784,180,384,us,United States,1,Cingular Wireless +310,784,620,1568,us,United States,1,Coleman County Telco /Trans TX +311,785,040,64,us,United States,1, +310,784,006,6,us,United States,1,Consolidated Telcom +310,784,060,1551,us,United States,1,Consolidated Telcom +312,786,380,896,us,United States,1, +310,784,930,2352,us,United States,1, +311,785,240,576,us,United States,1, +310,784,008,8,us,United States,1,Unknown +310,784,080,128,us,United States,1, +310,784,700,1792,us,United States,1,Cross Valliant Cellular Partnership +311,785,140,320,us,United States,1,Cross Wireless Telephone Co. +312,786,030,48,us,United States,1,Cross Wireless Telephone Co.2 +311,785,520,1312,us,United States,1, +311,785,810,2064,us,United States,1,Cumberland Cellular Partnership +311,785,800,2048,us,United States,1,Cumberland Cellular Partnership2 +311,785,440,1088,us,United States,1,Cumberland Cellular Partnership3 +312,786,040,64,us,United States,1,Custer Telephone Cooperative Inc. +310,784,016,22,us,United States,1,Denali Spectrum License LLC +310,784,440,1088,us,United States,1,Dobson Cellular Systems +310,784,990,2448,us,United States,1,E.N.M.R. Telephone Coop. +312,786,130,304,us,United States,1,East Kentucky Network LLC +312,786,120,288,us,United States,1,East Kentucky Network LLC-2 +310,784,750,1872,us,United States,1,East Kentucky Network LLC-3 +310,784,009,9,us,United States,1,Edge Wireless LLC +310,784,090,144,us,United States,1,Edge Wireless LLC +310,784,610,1552,us,United States,1,Elkhart TelCo. / Epic Touch Co. +311,785,210,528,us,United States,1, +311,785,311,785,us,United States,1,Farmers +311,785,460,1120,us,United States,1,Fisher Wireless Services Inc. +311,785,370,880,us,United States,1,GCI Communication Corp. +310,784,430,1072,us,United States,1,GCI Communication Corp.-2 +310,784,920,2336,us,United States,1,Get Mobile Inc. +310,784,970,2416,us,United States,1, +310,784,007,127,us,United States,1,Unknown +311,785,250,592,us,United States,1,i CAN_GSM +311,785,340,832,us,United States,1,Illinois Valley Cellular RSA 2 Partnership +311,785,030,48,us,United States,1, +312,786,170,368,us,United States,1,Iowa RSA No. 2 Limited Partnership +311,785,410,1040,us,United States,1,Iowa RSA No. 2 Limited Partnership2 +310,784,770,1904,us,United States,1,Iowa Wireless Services LLC +310,784,650,1616,us,United States,1,Jasper +310,784,870,2160,us,United States,1,Kaplan Telephone Company Inc. +311,785,810,2064,us,United States,1,Kentucky RSA #3 Cellular General Partnership +311,785,800,2048,us,United States,1,Kentucky RSA #3 Cellular General Partnership2 +311,785,440,1088,us,United States,1,Kentucky RSA #3 Cellular General Partnership3 +311,785,440,1088,us,United States,1,Kentucky RSA #4 Cellular General Partnership4 +311,785,810,2064,us,United States,1,Kentucky RSA #4 Cellular General Partnership5 +311,785,800,2048,us,United States,1,Kentucky RSA #4 Cellular General Partnership6 +312,786,180,384,us,United States,1,Keystone Wireless LLC +310,784,690,1680,us,United States,1,Keystone Wireless LLC-2 +311,785,310,784,us,United States,1,Lamar County Cellular +310,784,016,22,us,United States,1,LCW Wireless Operations LLC +310,784,016,22,us,United States,1,Leap Wireless International Inc. +311,785,090,144,us,United States,1, +310,784,040,64,us,United States,1,Matanuska Tel. Assn. Inc. +310,784,780,1920,us,United States,1,Message Express Co. / Airlink PCS +311,785,660,1632,us,United States,1, +311,785,330,816,us,United States,1,Michigan Wireless LLC +311,785,000,0,us,United States,1, +311,785,390,912,us,United States,1, +310,784,400,1024,us,United States,1,Minnesota South. Wirel. Co. / Hickory +312,786,010,16,us,United States,1,Missouri RSA No 5 Partnership +311,785,920,2336,us,United States,1,Missouri RSA No 5 Partnership2 +311,785,020,32,us,United States,1,Missouri RSA No 5 Partnership3 +311,785,010,16,us,United States,1,Missouri RSA No 5 Partnership4 +312,786,220,544,us,United States,1,Missouri RSA No 5 Partnership5 +310,784,350,848,us,United States,1,Mohave Cellular LP +310,784,570,1392,us,United States,1,MTPCS LLC +310,784,290,656,us,United States,1,NEP Cellcorp Inc. +310,784,034,847,us,United States,1,Nevada Wireless LLC +311,785,380,896,us,United States,1, +310,784,600,1536,us,United States,1,New-Cell Inc. +311,785,100,256,us,United States,1, +311,785,300,768,us,United States,1,Nexus Communications Inc. +310,784,130,304,us,United States,1,North Carolina RSA 3 Cellular Tel. Co. +312,786,230,560,us,United States,1,North Dakota Network Company +311,785,610,1552,us,United States,1,North Dakota Network Company2 +310,784,450,1104,us,United States,1,Northeast Colorado Cellular Inc. +311,785,710,1808,us,United States,1,Northeast Wireless Networks LLC +310,784,670,1648,us,United States,1,Northstar +310,784,011,17,us,United States,1,Northstar +311,785,420,1056,us,United States,1,Northwest Missouri Cellular Limited Partnership +310,784,540,1344,us,United States,1, +310,784,740,1856,us,United States,1, +310,784,760,1888,us,United States,1,Panhandle Telephone Cooperative Inc. +310,784,580,1408,us,United States,1,PCS ONE +311,785,170,368,us,United States,1,PetroCom +311,785,670,1648,us,United States,1,Pine Belt Cellular Inc. +311,785,080,128,us,United States,1, +310,784,790,1936,us,United States,1, +310,784,100,256,us,United States,1,Plateau Telecommunications Inc. +310,784,940,2368,us,United States,1,Poka Lambro Telco Ltd. +311,785,540,1344,us,United States,1, +311,785,730,1840,us,United States,1, +310,784,500,1280,us,United States,1,Public Service Cellular Inc. +312,786,160,352,us,United States,1,RSA 1 Limited Partnership +311,785,430,1072,us,United States,1,RSA 1 Limited Partnership +311,785,350,848,us,United States,1,Sagebrush Cellular Inc. +311,785,030,48,us,United States,1,Sagir Inc. +311,785,910,2320,us,United States,1, +310,784,046,1135,us,United States,1,SIMMETRY +311,785,260,608,us,United States,1,SLO Cellular Inc / Cellular One of San Luis +310,784,320,800,us,United States,1,Smith Bagley Inc. +310,784,015,351,us,United States,1,Unknown +316,790,011,17,us,United States,1,Southern Communications Services Inc. +310,784,002,2,us,United States,1,Sprint Spectrum +312,786,190,400,us,United States,1,Sprint Spectrum +311,785,880,2176,us,United States,1,Sprint Spectrum2 +311,785,870,2160,us,United States,1,Sprint Spectrum3 +311,785,490,1168,us,United States,1,Sprint Spectrum4 +310,784,120,288,us,United States,1,Sprint Spectrum5 +316,790,010,16,us,United States,1,Sprint Spectrum6 +310,784,031,799,us,United States,1,T-Mobile +310,784,220,544,us,United States,1,T-Mobile2 +310,784,270,624,us,United States,1,T-Mobile3 +310,784,210,528,us,United States,1,T-Mobile4 +310,784,260,608,us,United States,1,T-Mobile5 +310,784,200,512,us,United States,1,T-Mobile6 +310,784,250,592,us,United States,1,T-Mobile7 +310,784,160,352,us,United States,1,T-Mobile8 +310,784,240,576,us,United States,1,T-Mobile9 +310,784,660,1632,us,United States,1,T-Mobile10 +310,784,230,560,us,United States,1,T-Mobile11 +310,784,300,768,us,United States,1,T-Mobile12 +310,784,280,640,us,United States,1,T-Mobile13 +310,784,330,816,us,United States,1,T-Mobile14 +310,784,800,2048,us,United States,1,T-Mobile15 +310,784,310,784,us,United States,1,T-Mobile16 +311,785,740,1856,us,United States,1, +310,784,740,1856,us,United States,1,Telemetrix Inc. +310,784,014,335,us,United States,1,Testing +310,784,950,2384,us,United States,1,Unknown +310,784,860,2144,us,United States,1,Texas RSA 15B2 Limited Partnership +311,785,830,2096,us,United States,1,Thumb Cellular Limited Partnership +311,785,050,80,us,United States,1,Thumb Cellular Limited Partnership2 +310,784,460,1120,us,United States,1,TMP Corporation +310,784,490,1168,us,United States,1,Triton PCS +312,786,290,656,us,United States,1,Uintah Basin Electronics Telecommunications Inc. +311,785,860,2144,us,United States,1,Uintah Basin Electronics Telecommunications Inc.2 +310,784,960,2400,us,United States,1,Uintah Basin Electronics Telecommunications Inc.3 +310,784,020,32,us,United States,1,Union Telephone Co. +311,785,220,544,us,United States,1,United States Cellular Corp. +310,784,730,1840,us,United States,1,United States Cellular Corp.2 +311,785,650,1616,us,United States,1,United Wireless Communications Inc. +310,784,038,911,us,United States,1,USA 3650 AT&T +310,784,520,1312,us,United States,1,VeriSign +310,784,003,3,us,United States,1,Unknown +310,784,023,575,us,United States,1,Unknown2 +310,784,024,591,us,United States,1,Unknown3 +310,784,025,607,us,United States,1,Unknown4 +310,784,530,1328,us,United States,1,West Virginia Wireless +310,784,026,623,us,United States,1,Unknown +310,784,340,832,us,United States,1,Westlink Communications LLC +311,785,150,336,us,United States,1, +311,785,070,112,us,United States,1,Wisconsin RSA #7 Limited Partnership +310,784,390,912,us,United States,1,Yorkville Telephone Cooperative +748,1864,001,31,uy,Uruguay,598,Ancel/Antel +748,1864,003,63,uy,Uruguay,598,Ancel/Antel +748,1864,010,271,uy,Uruguay,598,Claro/AM Wireless +748,1864,007,127,uy,Uruguay,598,MOVISTAR +434,1076,004,79,uz,Uzbekistan,998,Bee Line/Unitel +434,1076,001,1,uz,Uzbekistan,998,Buztel +434,1076,007,127,uz,Uzbekistan,998,MTS/Uzdunrobita +434,1076,005,95,uz,Uzbekistan,998,Ucell/Coscom +434,1076,002,2,uz,Uzbekistan,998,Uzmacom +541,1345,005,95,vu,Vanuatu,678,DigiCel +541,1345,000,0,vu,Vanuatu,678,DigiCel2 +541,1345,001,31,vu,Vanuatu,678,SMILE +734,1844,003,3,ve,Venezuela,58,DigiTel C.A. +734,1844,002,47,ve,Venezuela,58,DigiTel C.A.2 +734,1844,001,1,ve,Venezuela,58,DigiTel C.A.3 +734,1844,006,111,ve,Venezuela,58,Movilnet C.A. +734,1844,004,79,ve,Venezuela,58,Movistar/TelCel +452,1106,007,127,vn,Viet Nam,84,Beeline +452,1106,001,31,vn,Viet Nam,84,Mobifone +452,1106,003,63,vn,Viet Nam,84,S-Fone/Telecom +452,1106,005,95,vn,Viet Nam,84,VietnaMobile +452,1106,008,143,vn,Viet Nam,84,Viettel Mobile +452,1106,006,111,vn,Viet Nam,84,Viettel Mobile2 +452,1106,004,79,vn,Viet Nam,84,Viettel Mobile3 +452,1106,002,47,vn,Viet Nam,84,Vinaphone +376,886,350,848,vi,Virgin Islands U.S.,1340,Cable & Wireless (Turks & Caicos) +376,886,050,1295,vi,Virgin Islands U.S.,1340,Digicel +376,886,352,850,vi,Virgin Islands U.S.,1340,IslandCom +421,1057,004,79,ye,Yemen,967,HITS/Y Unitel +421,1057,002,47,ye,Yemen,967,MTN/Spacetel +421,1057,001,31,ye,Yemen,967,Sabaphone +421,1057,003,63,ye,Yemen,967,Yemen Mob. CDMA +645,1605,003,63,zm,Zambia,260,Cell Z/MTS +645,1605,002,47,zm,Zambia,260,MTN/Telecel +645,1605,001,31,zm,Zambia,260,Zain/Celtel +648,1608,004,79,zw,Zimbabwe,263,Econet +648,1608,001,31,zw,Zimbabwe,263,Net One +648,1608,003,63,zw,Zimbabwe,263,Telecel diff --git a/src/pb_wa_messages.php b/src/pb_wa_messages.php new file mode 100644 index 00000000..46929cd4 --- /dev/null +++ b/src/pb_wa_messages.php @@ -0,0 +1,905 @@ + [ + 'name' => 'group_id', + 'required' => false, + 'type' => 7, + ], + self::SENDER_KEY => [ + 'name' => 'sender_key', + 'required' => false, + 'type' => 7, + ], + ]; + + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::GROUP_ID] = null; + $this->values[self::SENDER_KEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + public function getGroupId() + { + return $this->values[self::GROUP_ID]; + } + + public function getSenderKey() + { + return $this->values[self::SENDER_KEY]; + } + + public function setGroupId($id) + { + $this->values[self::GROUP_ID] = $id; + } + + public function setSenderKey($sender_key) + { + $this->values[self::SENDER_KEY] = $sender_key; + } +} +class SenderKeyGroupData extends \ProtobufMessage +{ + const MESSAGE = 1; + const SENDER_KEY = 2; + /* @var array Field descriptors */ + protected static $fields = [ + self::MESSAGE => [ + 'name' => 'message', + 'required' => false, + 'type' => 7, + ], + self::SENDER_KEY => [ + 'name' => 'sender_key', + 'required' => false, + 'type' => 'SenderKeyGroupMessage', + ], + + ]; + + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::MESSAGE] = null; + $this->values[self::SENDER_KEY] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + public function getMessage() + { + return $this->values[self::MESSAGE]; + } + + public function getSenderKey() + { + return $this->values[self::SENDER_KEY]; + } + + public function setMessage($data) + { + $this->values[self::MESSAGE] = $data; + } + + public function setSenderKey($sender_key) + { + $this->values[self::SENDER_KEY] = $sender_key; + } +} + +/* + $url = new MediaUrl(); + $url->parseFromString($data); +*/ +class MediaUrl extends \ProtobufMessage +{ + const MESSAGE = 1; //full message with the url + const URL = 2; // only the url + const UNK_1 = 3; + const UNK_2 = 4; + const DESCRIPTION = 5; //Metadata description + const TITLE = 6; //Page title + protected static $fields = [ + self::MESSAGE => [ + 'name' => 'message', + 'required' => false, + 'type' => 7, + ], + self::URL => [ + 'name' => 'url', + 'required' => false, + 'type' => 7, + ], + self::UNK_1 => [ + 'name' => 'unknown1', + 'required' => false, + 'type' => 5, + ], + self::UNK_1 => [ + 'name' => 'unknown2', + 'required' => false, + 'type' => 7, + ], + self::DESCRIPTION => [ + 'name' => 'description', + 'required' => false, + 'type' => 7, + ], + self::TITLE => [ + 'name' => 'title', + 'required' => false, + 'type' => 7, + ], + ]; + + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::MESSAGE] = null; + $this->values[self::URL] = null; + $this->values[self::UNK_1] = null; + $this->values[self::UNK_2] = null; + $this->values[self::DESCRIPTION] = null; + $this->values[self::TITLE] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + public function getMessage() + { + return $this->values[self::MESSAGE]; + } + + public function getUrl() + { + return $this->values[self::URL]; + } + + public function getUnknown1() + { + return $this->values[self::UNK_1]; + } + + public function getUnknown2() + { + return $this->values[self::UNK_2]; + } + + public function getDescription() + { + return $this->values[self::DESCRIPTION]; + } + + public function getTitle() + { + return $this->values[self::TITLE]; + } + + public function setMessage($value) + { + $this->values[self::MESSAGE] = $value; + } + + public function setUrl($value) + { + $this->values[self::URL] = $value; + } + + public function setUnknown1($value) + { + $this->values[self::UNK_1] = $value; + } + + public function setUnknown2($value) + { + $this->values[self::UNK_2] = $value; + } + + public function setDescription($value) + { + $this->values[self::DESCRIPTION] = $value; + } + + public function setTitle($value) + { + $this->values[self::TITLE] = $value; + } +} +class ImageMessage extends \ProtobufMessage +{ + const URL = 1; + const MIMETYPE = 2; + const CAPTION = 3; + const SHA256 = 4; + const LENGTH = 5; + const HEIGHT = 6; + const WIDTH = 7; + const REFKEY = 8; + const KEY = 9; + const IV = 10; + const THUMBNAIL = 11; + /* @var array Field descriptors */ + protected static $fields = [ + self::URL => [ + 'name' => 'url', + 'required' => false, + 'type' => 7, + ], + self::MIMETYPE => [ + 'name' => 'mimetype', + 'required' => false, + 'type' => 7, + ], + self::CAPTION => [ + 'name' => 'caption', + 'required' => false, + 'type' => 7, + ], + self::SHA256 => [ + 'name' => 'sha256', + 'required' => false, + 'type' => 7, + ], + self::LENGTH => [ + 'name' => 'length', + 'required' => false, + 'type' => 5, + ], + self::HEIGHT => [ + 'name' => 'height', + 'required' => false, + 'type' => 5, + ], + self::WIDTH => [ + 'name' => 'width', + 'required' => false, + 'type' => 5, + ], + self::REFKEY => [ + 'name' => 'refkey', + 'required' => false, + 'type' => 7, + ], + self::KEY => [ + 'name' => 'key', + 'required' => false, + 'type' => 7, + ], + self::IV => [ + 'name' => 'iv', + 'required' => false, + 'type' => 7, + ], + self::THUMBNAIL => [ + 'name' => 'thumbnail', + 'required' => false, + 'type' => 7, + ], + ]; + + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::URL] = null; + $this->values[self::MIMETYPE] = null; + $this->values[self::CAPTION] = null; + $this->values[self::SHA256] = null; + $this->values[self::LENGTH] = null; + $this->values[self::HEIGHT] = null; + $this->values[self::WIDTH] = null; + $this->values[self::REFKEY] = null; + $this->values[self::KEY] = null; + $this->values[self::IV] = null; + $this->values[self::THUMBNAIL] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + public function getUrl() + { + return $this->values[self::URL]; + } + + public function getMimeType() + { + return $this->values[self::MIMETYPE]; + } + + public function getCaption() + { + return $this->values[self::CAPTION]; + } + + public function getSha256() + { + return $this->values[self::SHA256]; + } + + public function getLength() + { + return $this->values[self::LENGTH]; + } + + public function getHeight() + { + return $this->values[self::HEIGHT]; + } + + public function getWidth() + { + return $this->values[self::WIDTH]; + } + + public function getRefKey() + { + return $this->values[self::REFKEY]; + } + + public function getKey() + { + return $this->values[self::KEY]; + } + + public function getIv() + { + return $this->values[self::IV]; + } + + public function getThumbnail() + { + return $this->values[self::THUMBNAIL]; + } + + public function setUrl($newValue) + { + $this->values[self::URL] = $newValue; + } + + public function setMimeType($newValue) + { + $this->values[self::MIMETYPE] = $newValue; + } + + public function setCaption($newValue) + { + $this->values[self::CAPTION] = $newValue; + } + + public function setSha256($newValue) + { + $this->values[self::SHA256] = $newValue; + } + + public function setLength($newValue) + { + $this->values[self::LENGTH] = $newValue; + } + + public function setHeight($newValue) + { + $this->values[self::HEIGHT] = $newValue; + } + + public function setWidth($newValue) + { + $this->values[self::WIDTH] = $newValue; + } + + public function setRefKey($newValue) + { + $this->values[self::REFKEY] = $newValue; + } + + public function setKey($newValue) + { + $this->values[self::KEY] = $newValue; + } + + public function setIv($newValue) + { + $this->values[self::IV] = $newValue; + } + + public function setThumbnail($newValue) + { + $this->values[self::THUMBNAIL] = $newValue; + } + + public function parseFromString($data) + { + parent::parseFromString($data); + $this->setThumbnail(stristr($data, hex2bin('ffd8ffe0'))); + } + + protected function WriteUInt32($val) + { + $result = ''; + $num1 = null; + while (true) { + $num1 = ($val & 127); + $val >>= 7; + if ($val != 0) { + $num2 = $num1 | 128; + $result .= chr($num2); + } else { + break; + } + } + $result .= chr($num1); + + return $result; + } + + public function serializeToString() + { + $thumb = $this->getThumbnail(); + $this->setThumbnail(null); + $data = parent::serializeToString(); + $data .= hex2bin('8201'); + $data .= $this->WriteUInt32(strlen($thumb)); + $data .= $thumb; + $this->setThumbnail($thumb); + + return $data; + } +} + +class Location extends \ProtobufMessage +{ + const LATITUDE = 1; + const LONGITUDE = 2; + const NAME = 3; + const DESCRIPTION = 4; + const URL = 5; + const THUMBNAIL = 6; + /* @var array Field descriptors */ + protected static $fields = [ + self::LATITUDE => [ + 'name' => 'Latitude', + 'required' => false, + 'type' => 1, + ], + self::LONGITUDE => [ + 'name' => 'Longitude', + 'required' => false, + 'type' => 1, + ], + self::NAME => [ + 'name' => 'Name', + 'required' => false, + 'type' => 7, + ], + self::DESCRIPTION => [ + 'name' => 'Description', + 'required' => false, + 'type' => 7, + ], + self::URL => [ + 'name' => 'Url', + 'required' => false, + 'type' => 7, + ], + self::THUMBNAIL => [ + 'name' => 'Thumbnail', + 'required' => false, + 'type' => 7, + ], + + ]; + + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::LATITUDE] = null; + $this->values[self::LONGITUDE] = null; + $this->values[self::NAME] = null; + $this->values[self::DESCRIPTION] = null; + $this->values[self::URL] = null; + $this->values[self::THUMBNAIL] = null; + } + + public function parseFromString($data) + { + parent::parseFromString($data); + $this->setThumbnail(stristr($data, hex2bin('ffd8ffe0'))); + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + public function getLatitude() + { + return $this->values[self::LATITUDE]; + } + + public function getLongitude() + { + return $this->values[self::LONGITUDE]; + } + + public function getThumbnail() + { + return $this->values[self::THUMBNAIL]; + } + + public function getName() + { + return $this->values[self::NAME]; + } + + public function getDescription() + { + return $this->values[self::DESCRIPTION]; + } + + public function getUrl() + { + return $this->values[self::URL]; + } + + public function setName($value) + { + $this->values[self::NAME] = $value; + } + + public function setDescription($value) + { + $this->values[self::DESCRIPTION] = $value; + } + + public function setUrl($value) + { + $this->values[self::URL] = $value; + } + + public function setLatitude($value) + { + $this->values[self::LATITUDE] = $value; + } + + public function setLongitude($value) + { + $this->values[self::LONGITUDE] = $value; + } + + public function setThumbnail($value) + { + $this->values[self::THUMBNAIL] = $value; + } + + protected function WriteUInt32($val) + { + $result = ''; + $num1 = null; + while (true) { + $num1 = ($val & 127); + $val >>= 7; + if ($val != 0) { + $num2 = $num1 | 128; + $result .= chr($num2); + } else { + break; + } + } + $result .= chr($num1); + + return $result; + } + + public function serializeToString() + { + $thumb = $this->getThumbnail(); + $this->setThumbnail(null); + $data = parent::serializeToString(); + $data .= hex2bin('8201'); + $data .= $this->WriteUInt32(strlen($thumb)); + $data .= $thumb; + $this->setThumbnail($thumb); + + return $data; + } +} + +/* May start with 01 thats bad */ +class DocumentMessage extends \ProtobufMessage +{ + const URL = 1; + const MIMETYPE = 2; + const NAME = 3; + const SHA256 = 4; + const LENGTH = 5; + const UNK_2 = 6; + const REFKEY = 7; + const FILENAME = 8; + const THUMBNAIL = 9; + /* @var array Field descriptors */ + protected static $fields = [ + self::URL => [ + 'name' => 'url', + 'required' => false, + 'type' => 7, + ], + self::MIMETYPE => [ + 'name' => 'mimetype', + 'required' => false, + 'type' => 7, + ], + self::NAME => [ + 'name' => 'name', + 'required' => false, + 'type' => 7, + ], + self::LENGTH => [ + 'name' => 'length', + 'required' => false, + 'type' => 5, + ], + self::SHA256 => [ + 'name' => 'sha256', + 'required' => false, + 'type' => 7, + ], + self::UNK_2 => [ + 'name' => 'UNK_2', + 'required' => false, + 'type' => 5, + ], + self::REFKEY => [ + 'name' => 'refkey', + 'required' => false, + 'type' => 7, + ], + self::FILENAME => [ + 'name' => 'filename', + 'required' => false, + 'type' => 7, + ], + self::THUMBNAIL => [ + 'name' => 'thumbnail', + 'required' => false, + 'type' => 7, + ], + ]; + + public function __construct() + { + $this->reset(); + } + + /** + * Clears message values and sets default ones. + * + * @return null + */ + public function reset() + { + $this->values[self::URL] = null; + $this->values[self::MIMETYPE] = null; + $this->values[self::NAME] = null; + $this->values[self::LENGTH] = null; + $this->values[self::SHA256] = null; + $this->values[self::UNK_2] = null; + $this->values[self::REFKEY] = null; + $this->values[self::FILENAME] = null; + $this->values[self::THUMBNAIL] = null; + } + + /** + * Returns field descriptors. + * + * @return array + */ + public function fields() + { + return self::$fields; + } + + public function getUrl() + { + return $this->values[self::URL]; + } + + public function getMimeType() + { + return $this->values[self::MIMETYPE]; + } + + public function getLength() + { + return $this->values[self::LENGTH]; + } + + public function getName() + { + return $this->values[self::NAME]; + } + + public function getUNK2() + { + return $this->values[self::UNK2]; + } + + public function getRefKey() + { + return $this->values[self::REFKEY]; + } + + public function getFilename() + { + return $this->values[self::FILENAME]; + } + + public function getThumbnail() + { + return $this->values[self::THUMBNAIL]; + } + + public function setUrl($newValue) + { + $this->values[self::URL] = $newValue; + } + + public function setMimeType($newValue) + { + $this->values[self::MIMETYPE] = $newValue; + } + + public function setName($newValue) + { + $this->values[self::NAME] = $newValue; + } + + public function setSha256($newValue) + { + $this->values[self::SHA256] = $newValue; + } + + public function setLength($newValue) + { + $this->values[self::LENGTH] = $newValue; + } + + public function setRefKey($newValue) + { + $this->values[self::REFKEY] = $newValue; + } + + public function setThumbnail($newValue) + { + $this->values[self::THUMBNAIL] = $newValue; + } + + public function parseFromString($data) + { + parent::parseFromString($data); + $this->setThumbnail(stristr($data, hex2bin('ffd8ffe0'))); + } + + protected function WriteUInt32($val) + { + $result = ''; + $num1 = null; + while (true) { + $num1 = ($val & 127); + $val >>= 7; + if ($val != 0) { + $num2 = $num1 | 128; + $result .= chr($num2); + } else { + break; + } + } + $result .= chr($num1); + + return $result; + } + + public function serializeToString() + { + $thumb = $this->getThumbnail(); + $this->setThumbnail(null); + $data = parent::serializeToString(); + $data .= hex2bin('8201'); + $data .= $this->WriteUInt32(strlen($thumb)); + $data .= $thumb; + $this->setThumbnail($thumb); + + return $data; + } +} diff --git a/src/protocol.class.php b/src/protocol.class.php index ed352483..7a089b17 100755 --- a/src/protocol.class.php +++ b/src/protocol.class.php @@ -1,5 +1,5 @@ input; } - } class ProtocolNode @@ -32,23 +31,21 @@ class ProtocolNode private static $cli = null; /** - * check if call is from command line + * check if call is from command line. + * * @return bool */ private static function isCli() { - if(self::$cli === null) - { + if (self::$cli === null) { //initial setter - if(php_sapi_name() == "cli") - { + if (php_sapi_name() == 'cli') { self::$cli = true; - } - else - { + } else { self::$cli = false; } } + return self::$cli; } @@ -84,6 +81,46 @@ public function getChildren() return $this->children; } + /** + * @param ProtocolNode $node + */ + public function addChild(ProtocolNode $node) + { + $this->children[] = $node; + } + + /** + * @param ProtocolNode $node + */ + public function removeChild($tag, $attrs = []) + { + if ($this->children) { + if (is_int($tag)) { + if (isset($this->children[$tag])) { + array_slice($this->children, $tag, 1); + } + } else { + foreach ($this->children as $i => $child) { + $index = -1; + if (strcmp($child->tag, $tag) == 0) { + $index = $i; + foreach ($attrs as $key => $val) { + if (strcmp($child->getAttribute($key), $val) != 0) { + $index = -1; // attrs not equal + break; + } + } + } + if ($index != -1) { + array_slice($this->children, $index, 1); + + return; + } + } + } + } + } + public function __construct($tag, $attributeHash, $children, $data) { $this->tag = $tag; @@ -94,55 +131,60 @@ public function __construct($tag, $attributeHash, $children, $data) /** * @param string $indent - * @param bool $isChild + * @param bool $isChild + * * @return string */ - public function nodeString($indent = "", $isChild = false) + public function nodeString($indent = '', $isChild = false) { + // non printable characters regex + $nonPrintable = '#[^\x20-\x7E]#'; + //formatters - $lt = "<"; - $gt = ">"; + $lt = '<'; + $gt = '>'; $nl = "\n"; - if(!self::isCli()) - { - $lt = "<"; - $gt = ">"; - $nl = "
"; - $indent = str_replace(" ", " ", $indent); + if (!self::isCli()) { + $lt = '<'; + $gt = '>'; + $nl = '
'; + $indent = str_replace(' ', ' ', $indent); } - $ret = $indent . $lt . $this->tag; + $ret = $indent.$lt.$this->tag; if ($this->attributeHash != null) { foreach ($this->attributeHash as $key => $value) { - $ret .= " " . $key . "=\"" . $value . "\""; + $ret .= ' '.$key.'="'.$value.'"'; } } $ret .= $gt; if (strlen($this->data) > 0) { if (strlen($this->data) <= 1024) { //message - $ret .= $this->data; + if (preg_match($nonPrintable, $this->data)) { + $ret .= bin2hex($this->data); + } else { + $ret .= $this->data; + } } else { //raw data - $ret .= " " . strlen($this->data) . " byte data"; + $ret .= ' '.strlen($this->data).' byte data'; } } if ($this->children) { $ret .= $nl; - $foo = array(); + $foo = []; foreach ($this->children as $child) { - $foo[] = $child->nodeString($indent . " ", true); + $foo[] = $child->nodeString($indent.' ', true); } $ret .= implode($nl, $foo); - $ret .= $nl . $indent; + $ret .= $nl.$indent; } - $ret .= $lt . "/" . $this->tag . $gt; + $ret .= $lt.'/'.$this->tag.$gt; - if(!$isChild) - { + if (!$isChild) { $ret .= $nl; - if(!self::isCli()) - { + if (!self::isCli()) { $ret .= $nl; } } @@ -152,11 +194,12 @@ public function nodeString($indent = "", $isChild = false) /** * @param $attribute + * * @return string */ public function getAttribute($attribute) { - $ret = ""; + $ret = ''; if (isset($this->attributeHash[$attribute])) { $ret = $this->attributeHash[$attribute]; } @@ -166,49 +209,57 @@ public function getAttribute($attribute) /** * @param string $needle - * @return boolean + * + * @return bool */ public function nodeIdContains($needle) { - return (strpos($this->getAttribute("id"), $needle) !== false); + return strpos($this->getAttribute('id'), $needle) !== false; } //get children supports string tag or int index + /** * @param $tag + * @param array $attrs + * * @return ProtocolNode */ - public function getChild($tag) + public function getChild($tag, $attrs = []) { $ret = null; if ($this->children) { - if(is_int($tag)) - { - if(isset($this->children[$tag])) - { + if (is_int($tag)) { + if (isset($this->children[$tag])) { return $this->children[$tag]; - } - else - { - return null; + } else { + return; } } foreach ($this->children as $child) { if (strcmp($child->tag, $tag) == 0) { - return $child; + $found = true; + foreach ($attrs as $key => $value) { + if (strcmp($child->getAttribute($key), $value) != 0) { + $found = false; + break; + } + } + if ($found) { + return $child; + } } - $ret = $child->getChild($tag); + $ret = $child->getChild($tag, $attrs); if ($ret) { return $ret; } } } - - return null; } /** * @param $tag + * * @return bool */ public function hasChild($tag) @@ -231,478 +282,21 @@ public function refreshTimes($offset = 0) $this->attributeHash['t'] = time(); } } - - + /** - * Print human readable ProtocolNode object + * Print human readable ProtocolNode object. * * @return string */ public function __toString() { - $readableNode = array( + $readableNode = [ 'tag' => $this->tag, 'attributeHash' => $this->attributeHash, 'children' => $this->children, - 'data' => $this->data - ); - - return print_r( $readableNode, true ); - } - -} - -class BinTreeNodeReader -{ - private $input; - /** @var $key KeyStream */ - private $key; - - public function resetKey() - { - $this->key = null; - } - - public function setKey($key) - { - $this->key = $key; - } - - public function nextTree($input = null) - { - if ($input != null) { - $this->input = $input; - } - $firstByte = $this->peekInt8(); - $stanzaFlag = ($firstByte & 0xF0) >> 4; - $stanzaSize = $this->peekInt16(1) | (($firstByte & 0x0F) << 16); - if ($stanzaSize > strlen($this->input)) { - throw new Exception("Incomplete message $stanzaSize != " . strlen($this->input)); - } - $this->readInt24(); - if ($stanzaFlag & 8) { - if (isset($this->key)) { - $realSize = $stanzaSize - 4; - $this->input = $this->key->DecodeMessage($this->input, $realSize, 0, $realSize);// . $remainingData; - } else { - throw new Exception("Encountered encrypted message, missing key"); - } - } - if ($stanzaSize > 0) { - return $this->nextTreeInternal(); - } - - return null; - } - - protected function getToken($token) - { - $ret = ""; - $subdict = false; - TokenMap::GetToken($token, $subdict, $ret); - if(!$ret) - { - $token = $this->readInt8(); - TokenMap::GetToken($token, $subdict, $ret); - if(!$ret) - { - throw new Exception("BinTreeNodeReader->getToken: Invalid token $token"); - } - } - return $ret; - } - - protected function readString($token) - { - $ret = ""; - if ($token == -1) { - throw new Exception("BinTreeNodeReader->readString: Invalid token $token"); - } - if (($token > 4) && ($token < 0xf5)) { - $ret = $this->getToken($token); - } elseif ($token == 0) { - $ret = ""; - } elseif ($token == 0xfc) { - $size = $this->readInt8(); - $ret = $this->fillArray($size); - } elseif ($token == 0xfd) { - $size = $this->readInt24(); - $ret = $this->fillArray($size); - } elseif ($token == 0xfe) { - $token = $this->readInt8(); - $ret = $this->getToken($token + 0xf5); - } elseif ($token == 0xfa) { - $user = $this->readString($this->readInt8()); - $server = $this->readString($this->readInt8()); - if ((strlen($user) > 0) && (strlen($server) > 0)) { - $ret = $user . "@" . $server; - } elseif (strlen($server) > 0) { - $ret = $server; - } - } - - return $ret; - } - - protected function readAttributes($size) - { - $attributes = array(); - $attribCount = ($size - 2 + $size % 2) / 2; - for ($i = 0; $i < $attribCount; $i++) { - $key = $this->readString($this->readInt8()); - $value = $this->readString($this->readInt8()); - $attributes[$key] = $value; - } - - return $attributes; - } - - protected function nextTreeInternal() - { - $token = $this->readInt8(); - $size = $this->readListSize($token); - $token = $this->readInt8(); - if ($token == 1) { - $attributes = $this->readAttributes($size); - - return new ProtocolNode("start", $attributes, null, ""); - } elseif ($token == 2) { - return null; - } - $tag = $this->readString($token); - $attributes = $this->readAttributes($size); - if (($size % 2) == 1) { - return new ProtocolNode($tag, $attributes, null, ""); - } - $token = $this->readInt8(); - if ($this->isListTag($token)) { - return new ProtocolNode($tag, $attributes, $this->readList($token), ""); - } - - return new ProtocolNode($tag, $attributes, null, $this->readString($token)); - } - - protected function isListTag($token) - { - return (($token == 248) || ($token == 0) || ($token == 249)); - } - - protected function readList($token) - { - $size = $this->readListSize($token); - $ret = array(); - for ($i = 0; $i < $size; $i++) { - array_push($ret, $this->nextTreeInternal()); - } - - return $ret; - } - - protected function readListSize($token) - { - $size = 0; - if ($token == 0xf8) { - $size = $this->readInt8(); - } elseif ($token == 0xf9) { - $size = $this->readInt16(); - } else { - throw new Exception("BinTreeNodeReader->readListSize: Invalid token $token"); - } - - return $size; - } - - protected function peekInt24($offset = 0) - { - $ret = 0; - if (strlen($this->input) >= (3 + $offset)) { - $ret = ord(substr($this->input, $offset, 1)) << 16; - $ret |= ord(substr($this->input, $offset + 1, 1)) << 8; - $ret |= ord(substr($this->input, $offset + 2, 1)) << 0; - } - - return $ret; - } - - protected function readInt24() - { - $ret = $this->peekInt24(); - if (strlen($this->input) >= 3) { - $this->input = substr($this->input, 3); - } - - return $ret; - } - - protected function peekInt16($offset = 0) - { - $ret = 0; - if (strlen($this->input) >= (2 + $offset)) { - $ret = ord(substr($this->input, $offset, 1)) << 8; - $ret |= ord(substr($this->input, $offset + 1, 1)) << 0; - } - - return $ret; - } - - protected function readInt16() - { - $ret = $this->peekInt16(); - if ($ret > 0) { - $this->input = substr($this->input, 2); - } - - return $ret; - } - - protected function peekInt8($offset = 0) - { - $ret = 0; - if (strlen($this->input) >= (1 + $offset)) { - $sbstr = substr($this->input, $offset, 1); - $ret = ord($sbstr); - } - - return $ret; - } - - protected function readInt8() - { - $ret = $this->peekInt8(); - if (strlen($this->input) >= 1) { - $this->input = substr($this->input, 1); - } - - return $ret; - } - - protected function fillArray($len) - { - $ret = ""; - if (strlen($this->input) >= $len) { - $ret = substr($this->input, 0, $len); - $this->input = substr($this->input, $len); - } - - return $ret; - } - -} - -class BinTreeNodeWriter -{ - private $output; - /** @var $key KeyStream */ - private $key; - - public function resetKey() - { - $this->key = null; - } - - public function setKey($key) - { - $this->key = $key; - } - - public function StartStream($domain, $resource) - { - $attributes = array(); - $header = "WA"; - $header .= $this->writeInt8(1); - $header .= $this->writeInt8(4); - - $attributes["to"] = $domain; - $attributes["resource"] = $resource; - $this->writeListStart(count($attributes) * 2 + 1); - - $this->output .= "\x01"; - $this->writeAttributes($attributes); - $ret = $header . $this->flushBuffer(); - - return $ret; - } - - /** - * @param ProtocolNode $node - * @return string - */ - public function write($node, $encrypt = true) - { - if ($node == null) { - $this->output .= "\x00"; - } else { - $this->writeInternal($node); - } - - return $this->flushBuffer($encrypt); - } - - /** - * @param ProtocolNode $node - */ - protected function writeInternal($node) - { - $len = 1; - if ($node->getAttributes() != null) { - $len += count($node->getAttributes()) * 2; - } - if (count($node->getChildren()) > 0) { - $len += 1; - } - if (strlen($node->getData()) > 0) { - $len += 1; - } - $this->writeListStart($len); - $this->writeString($node->getTag()); - $this->writeAttributes($node->getAttributes()); - if (strlen($node->getData()) > 0) { - $this->writeBytes($node->getData()); - } - if ($node->getChildren()) { - $this->writeListStart(count($node->getChildren())); - foreach ($node->getChildren() as $child) { - $this->writeInternal($child); - } - } - } - - protected function parseInt24($data) - { - $ret = ord(substr($data, 0, 1)) << 16; - $ret |= ord(substr($data, 1, 1)) << 8; - $ret |= ord(substr($data, 2, 1)) << 0; - return $ret; - } - - protected function flushBuffer($encrypt = true) - { - $size = strlen($this->output); - $data = $this->output; - if($this->key != null && $encrypt) - { - $bsize = $this->getInt24($size); - //encrypt - $data = $this->key->EncodeMessage($data, $size, 0, $size); - $len = strlen($data); - $bsize[0] = chr((8 << 4) | (($len & 16711680) >> 16)); - $bsize[1] = chr(($len & 65280) >> 8); - $bsize[2] = chr($len & 255); - $size = $this->parseInt24($bsize); - } - $ret = $this->writeInt24($size) . $data; - $this->output = ''; - return $ret; - } - - protected function getInt24($length) - { - $ret = ''; - $ret .= chr((($length & 0xf0000) >> 16)); - $ret .= chr((($length & 0xff00) >> 8)); - $ret .= chr(($length & 0xff)); - return $ret; - } + 'data' => $this->data, + ]; - protected function writeToken($token) - { - if ($token < 0xf5) { - $this->output .= chr($token); - } elseif ($token <= 0x1f4) { - $this->output .= "\xfe" . chr($token - 0xf5); - } + return print_r($readableNode, true); } - - protected function writeJid($user, $server) - { - $this->output .= "\xfa"; - if (strlen($user) > 0) { - $this->writeString($user); - } else { - $this->writeToken(0); - } - $this->writeString($server); - } - - protected function writeInt8($v) - { - $ret = chr($v & 0xff); - - return $ret; - } - - protected function writeInt16($v) - { - $ret = chr(($v & 0xff00) >> 8); - $ret .= chr(($v & 0x00ff) >> 0); - - return $ret; - } - - protected function writeInt24($v) - { - $ret = chr(($v & 0xff0000) >> 16); - $ret .= chr(($v & 0x00ff00) >> 8); - $ret .= chr(($v & 0x0000ff) >> 0); - - return $ret; - } - - protected function writeBytes($bytes) - { - $len = strlen($bytes); - if ($len >= 0x100) { - $this->output .= "\xfd"; - $this->output .= $this->writeInt24($len); - } else { - $this->output .= "\xfc"; - $this->output .= $this->writeInt8($len); - } - $this->output .= $bytes; - } - - protected function writeString($tag) - { - $intVal = -1; - $subdict = false; - if(TokenMap::TryGetToken($tag, $subdict, $intVal)) - { - if($subdict) - { - $this->writeToken(236); - } - $this->writeToken($intVal); - return; - } - $index = strpos($tag, '@'); - if ($index) { - $server = substr($tag, $index + 1); - $user = substr($tag, 0, $index); - $this->writeJid($user, $server); - } else { - $this->writeBytes($tag); - } - } - - protected function writeAttributes($attributes) - { - if ($attributes) { - foreach ($attributes as $key => $value) { - $this->writeString($key); - $this->writeString($value); - } - } - } - - protected function writeListStart($len) - { - if ($len == 0) { - $this->output .= "\x00"; - } elseif ($len < 256) { - $this->output .= "\xf8" . chr($len); - } else { - $this->output .= "\xf9" . $this->writeInt16($len); - } - } - } diff --git a/src/rc4.php b/src/rc4.php index c882e21e..79db5620 100755 --- a/src/rc4.php +++ b/src/rc4.php @@ -41,7 +41,6 @@ protected function swap($i, $j) $this->s[$i] = $this->s[$j]; $this->s[$j] = $c; } - } // DEPRECATED: WAUTH-1 diff --git a/src/smileys.class.php b/src/smileys.class.php new file mode 100644 index 00000000..081c5c04 --- /dev/null +++ b/src/smileys.class.php @@ -0,0 +1,919 @@ +SMILEY_XXX where XXX is the index): + * $s = new Smileys; + * echo $s->GRINNING_FACE_WITH_SMILING_EYES; + * // or + * echo $s->SMILEY_23; + * + * Complete list with images: + * http://www.typografie.info/3/page/artikel.htm/_/wissen/unicode-emoji-deutsch + * http://appamatix.com/843-whatsapp-emoticons-meanings-emoji-list/ + */ +class SmileyNotFoundException extends Exception +{ +}; + +class Smileys +{ + private $listSmileys = [ + 'SMILING_FACE_WITH_OPEN_MOUTH_AND_SMILING_EYES' => 0x1F604, // Smiley 1 + 'SMILING_FACE_WITH_OPEN_MOUTH' => 0x1F603, // Smiley 2 + 'GRINNING_FACE_WITH_SMILING_EYES' => 0x1F600, // Smiley 3 + 'SMILING_FACE_WITH_SMILING_EYES' => 0x1F60A, // Smiley 4 + 'WHITE_SMILING_FACE' => 0x263A, // Smiley 5 + 'WINKING_FACE' => 0x1F609, // Smiley 6 + 'SMILING_FACE_WITH_HEART_SHAPED_EYES' => 0x1F60D, // Smiley 7 + 'FACE_THROWING_A_KISS' => 0x1F618, // Smiley 8 + 'KISSING_FACE_WITH_CLOSED_EYES' => 0x1F61A, // Smiley 9 + 'KISSING_FACE' => 0x1F617, // Smiley 10 + 'KISSING_FACE_WITH_SMILING_EYES' => 0x1F619, // Smiley 11 + 'FACE_WITH_STUCK_OUT_TONGUE_AND_WINKING_EYE' => 0x1F61C, // Smiley 12 + 'FACE_WITH_STUCK_OUT_TONGUE_AND_TIGHTLY_CLOSED_EYES' => 0x1F61D, // Smiley 13 + 'FACE_WITH_STUCK_OUT_TONGUE' => 0x1F61B, // Smiley 14 + 'FLUSHED_FACE' => 0x1F633, // Smiley 15 + 'GRINNING_FACE_WITH_SMILING_EYES' => 0x1F601, // Smiley 16 + 'PENSIVE_FACE' => 0x1F614, // Smiley 17 + 'RELIEVED_FACE' => 0x1F60C, // Smiley 18 + 'UNAMUSED_FACE' => 0x1F612, // Smiley 19 + 'DISAPPOINTED_FACE' => 0x1F61E, // Smiley 20 + 'PERSEVERING_FACE' => 0x1F623, // Smiley 21 + 'CRYING_FACE' => 0x1F622, // Smiley 22 + 'FACE_WITH_TEARS_OF_JOY' => 0x1F602, // Smiley 23 + 'LOUDLY_CRYING_FACE' => 0x1F62D, // Smiley 24 + 'SLEEPY_FACE' => 0x1F62A, // Smiley 25 + 'DISAPPOINTED_BUT_RELIEVED_FACE' => 0x1F625, // Smiley 26 + 'FACE_WITH_OPEN_MOUTH_AND_COLD_SWEAT' => 0x1F630, // Smiley 27 + 'SMILING_FACE_WITH_OPEN_MOUTH_AND_COLD_SWEAT' => 0x1F605, // Smiley 28 + 'FACE_WITH_COLD_SWEAT' => 0x1F613, // Smiley 29 + 'WEARY_FACE' => 0x1F629, // Smiley 30 + 'TIRED_FACE' => 0x1F62B, // Smiley 31 + 'FEARFUL_FACE' => 0x1F628, // Smiley 32 + 'FACE_SCREAMING_IN_FEAR' => 0x1F631, // Smiley 33 + 'ANGRY_FACE' => 0x1F620, // Smiley 34 + 'POUTING_FACE' => 0x1F621, // Smiley 35 + 'FACE_WITH_LOOK_OF_TRIUMPH' => 0x1F624, // Smiley 36 + 'CONFOUNDED_FACE' => 0x1F616, // Smiley 37 + 'SMILING_FACE_WITH_OPEN_MOUTH_AND_TIGHTLY_CLOSED_EYES' => 0x1F606, // Smiley 38 + 'FACE_SAVOURING_DELICIOUS_FOOD' => 0x1F60B, // Smiley 39 + 'FACE_WITH_MEDICAL_MASK' => 0x1F637, // Smiley 40 + 'SMILING_FACE_WITH_SUNGLASSES' => 0x1F60E, // Smiley 41 + 'SLEEPING_FACE' => 0x1F634, // Smiley 42 + 'DIZZY_FACE' => 0x1F635, // Smiley 43 + 'ASTONISHED_FACE' => 0x1F632, // Smiley 44 + 'WORRIED_FACE' => 0x1F61F, // Smiley 45 + 'FROWNING_FACE_WITH_OPEN_MOUTH' => 0x1F626, // Smiley 46 + 'ANGUISHED_FACE' => 0x1F627, // Smiley 47 + 'SMILING_FACE_WITH_HORNS' => 0x1F608, // Smiley 48 + 'IMP' => 0x1F47F, // Smiley 49 + 'FACE_WITH_OPEN_MOUTH' => 0x1F62E, // Smiley 50 + 'GRIMACING_FACE' => 0x1F62C, // Smiley 51 + 'NEUTRAL_FACE' => 0x1F610, // Smiley 52 + 'CONFUSED_FACE' => 0x1F615, // Smiley 53 + 'HUSHED_FACE' => 0x1F62F, // Smiley 54 + 'FACE_WITHOUT_MOUTH' => 0x1F636, // Smiley 55 + 'SMILING_FACE_WITH_HALO' => 0x1F607, // Smiley 56 + 'SMIRKING_FACE' => 0x1F60F, // Smiley 57 + 'EXPRESSIONLESS_FACE' => 0x1F611, // Smiley 58 + 'MAN_WITH_GUA_PI_MAO' => 0x1F472, // Smiley 59 + 'MAN_WITH_TURBAN' => 0x1F473, // Smiley 60 + 'POLICE_OFFICER' => 0x1F46E, // Smiley 61 + 'CONSTRUCTION_WORKER' => 0x1F477, // Smiley 62 + 'GUARDSMAN' => 0x1F482, // Smiley 63 + 'BABY' => 0x1F476, // Smiley 64 + 'BOY' => 0x1F466, // Smiley 65 + 'GIRL' => 0x1F467, // Smiley 66 + 'MAN' => 0x1F468, // Smiley 67 + 'WOMAN' => 0x1F469, // Smiley 68 + 'OLDER_MAN' => 0x1F474, // Smiley 69 + 'OLDER_WOMAN' => 0x1F475, // Smiley 70 + 'PERSON_WITH_BLONDE_HAIR' => 0x1F471, // Smiley 71 + 'BABY_ANGEL' => 0x1F47C, // Smiley 72 + 'PRINCESS' => 0x1F478, // Smiley 73 + 'SMILING_CAT_FACE_WITH_OPEN_MOUTH' => 0x1F63A, // Smiley 74 + 'GRINNING_CAT_FACE_WITH_SMILING_EYES' => 0x1F638, // Smiley 75 + 'SMILING_FACE_FACE_WITH_HEART_SHAPED_EYES' => 0x1F63B, // Smiley 76 + 'KISSING_CAT_FACE_WITH_CLOSED_EYES' => 0x1F63D, // Smiley 77 + 'CAT_FACE_WITH_WRY_SMILE' => 0x1F63C, // Smiley 78 + 'WEARY_CAT_FACE' => 0x1F640, // Smiley 79 + 'CRYING_CAT_FACE' => 0x1F63F, // Smiley 80 + 'CAT_FACE_WITH_TEARS_OF_JOY' => 0x1F639, // Smiley 81 + 'POUTING_CAT_FACE' => 0x1F63E, // Smiley 82 + 'JAPANESE_OGRE' => 0x1F479, // Smiley 83 + 'JAPANESE_GOBLIN' => 0x1F47A, // Smiley 84 + 'SEE_NO_EVIL_MONKEY' => 0x1F648, // Smiley 85 + 'HEAR_NO_EVIL_MONKEY' => 0x1F649, // Smiley 86 + 'SPEAK_NO_EVIL_MONKEY' => 0x1F64A, // Smiley 87 + 'SKULL' => 0x1F480, // Smiley 88 + 'EXTRATERRESTRIAL_ALIEN' => 0x1F47D, // Smiley 89 + 'PILE_OF_POO' => 0x1F4A9, // Smiley 90 + 'FIRE' => 0x1F525, // Smiley 91 + 'SPARKLES' => 0x2728, // Smiley 92 + 'GLOWING_STAR' => 0x1F31F, // Smiley 93 + 'DIZZY_SYMBOL' => 0x1F4AB, // Smiley 94 + 'COLLISION_SYMBOL' => 0x1F4A5, // Smiley 95 + 'ANGER_SYMBOL' => 0x1F4A2, // Smiley 96 + 'SPLASHING_SWEAT_SYMBOL' => 0x1F4A6, // Smiley 97 + 'DROPLET' => 0x1F4A7, // Smiley 98 + 'SLEEPING_SYMBOL' => 0x1F4A4, // Smiley 99 + 'DASH_SYMBOL' => 0x1F4A8, // Smiley 100 + 'EAR' => 0x1F442, // Smiley 101 + 'EYES' => 0x1F440, // Smiley 102 + 'NOSE' => 0x1F443, // Smiley 103 + 'TONGUE' => 0x1F445, // Smiley 104 + 'MOUTH' => 0x1F444, // Smiley 105 + 'THUMBS_UP_SIGN' => 0x1F44D, // Smiley 106 + 'THUMBS_DOWN_SIGN' => 0x1F44E, // Smiley 107 + 'OK_HAND_SIGN' => 0x1F44C, // Smiley 108 + 'FISTED_HAND_SIGN' => 0x1F44A, // Smiley 109 + 'RAISED_FIST' => 0x270A, // Smiley 110 + 'VICTORY_HAND' => 0x270C, // Smiley 111 + 'WAVING_HAND_SIGN' => 0x1F44B, // Smiley 112 + 'RAISED_HAND' => 0x270B, // Smiley 113 + 'OPEN_HANDS_SIGN' => 0x1F450, // Smiley 114 + 'WHITE_UP_POINTING_BACKHAND_INDEX' => 0x1F446, // Smiley 115 + 'WHITE_DOWN_POINTING_BACKHAND_INDEX' => 0x1F447, // Smiley 116 + 'WHITE_RIGHT_POINTING_BACKHAND_INDEX' => 0x1F449, // Smiley 117 + 'WHITE_LEFT_POINTING_BACKHAND_INDEX' => 0x1F448, // Smiley 118 + 'PERSON_RAISING_BOTH_HANDS_IN_CELEBRATION' => 0x1F64C, // Smiley 119 + 'PERSON_WITH_FOLDED_HANDS' => 0x1F64F, // Smiley 120 + 'WHITE_UP_POINTING_INDEX' => 0x261D, // Smiley 121 + 'CLAPPING_HANDS_SIGN' => 0x1F44F, // Smiley 122 + 'FLEXED_BICEPS' => 0x1F4AA, // Smiley 123 + 'PEDESTRIAN' => 0x1F6B6, // Smiley 124 + 'RUNNER' => 0x1F3C3, // Smiley 125 + 'DANCER' => 0x1F483, // Smiley 126 + 'MAN_AND_WOMAN_HOLDING_HANDS' => 0x1F46B, // Smiley 127 + 'FAMILY' => 0x1F46A, // Smiley 128 + 'TWO_MEN_HOLDING_HANDS' => 0x1F46C, // Smiley 129 + 'TWO_WOMEN_HOLDING_HANDS' => 0x1F46D, // Smiley 130 + 'KISS' => 0x1F48F, // Smiley 131 + 'COUPLE_WITH_HEART' => 0x1F491, // Smiley 132 + 'WOMEN_WITH_BUNNY_EARS' => 0x1F46F, // Smiley 133 + 'FACE_WITH_OK_GESTURE' => 0x1F646, // Smiley 134 + 'FACE_WITH_NO_GOOD_GESTURE' => 0x1F645, // Smiley 135 + 'INFORMATION_DESK_PERSON' => 0x1F481, // Smiley 136 + 'HAPPY_PERSON_RAISING_ONE_HAND' => 0x1F64B, // Smiley 137 + 'FACE_MASSAGE' => 0x1F486, // Smiley 138 + 'HAIRCUT' => 0x1F487, // Smiley 139 + 'NAIL_POLISH' => 0x1F485, // Smiley 140 + 'BRIDE_WITH_VEIL' => 0x1F470, // Smiley 141 + 'PERSON_WITH_POUTING_FACE' => 0x1F64E, // Smiley 142 + 'PERSON_FROWNING' => 0x1F64D, // Smiley 143 + 'PERSON_BOWING_DEEPLY' => 0x1F647, // Smiley 144 + 'TOP_HAT' => 0x1F3A9, // Smiley 145 + 'CROWN' => 0x1F451, // Smiley 146 + 'WOMANS_HAT' => 0x1F452, // Smiley 147 + 'ATHLETIC_SHOE' => 0x1F45F, // Smiley 148 + 'MENS_SHOE' => 0x1F45E, // Smiley 149 + 'WOMANS_SANDAL' => 0x1F461, // Smiley 150 + 'HIGH_HEELED_SHOE' => 0x1F460, // Smiley 151 + 'WOMANS_BOOTS' => 0x1F462, // Smiley 152 + 'T_SHIRT' => 0x1F455, // Smiley 153 + 'NECKTIE' => 0x1F454, // Smiley 154 + 'WOMANS_CLOTHES' => 0x1F45A, // Smiley 155 + 'DRESS' => 0x1F457, // Smiley 156 + 'RUNNING_SHIRT_WITH_SASH' => 0x1F3BD, // Smiley 157 + 'JEANS' => 0x1F456, // Smiley 158 + 'KIMONO' => 0x1F458, // Smiley 159 + 'BIKINI' => 0x1F459, // Smiley 160 + 'BRIEFCASE' => 0x1F4BC, // Smiley 161 + 'HANDBAG' => 0x1F45C, // Smiley 162 + 'POUCH' => 0x1F45D, // Smiley 163 + 'PURSE' => 0x1F45B, // Smiley 164 + 'EYEGLASSES' => 0x1F453, // Smiley 165 + 'RIBBON' => 0x1F380, // Smiley 166 + 'CLOSED_UMBRELLA' => 0x1F302, // Smiley 167 + 'LIPSTICK' => 0x1F484, // Smiley 168 + 'YELLOW_HEART' => 0x1F49B, // Smiley 169 + 'BLUE_HEART' => 0x1F499, // Smiley 170 + 'PURPLE_HEART' => 0x1F49C, // Smiley 171 + 'GREEN_HEART' => 0x1F49A, // Smiley 172 + 'HEAVY_BLACK_HEART' => 0x2764, // Smiley 173 + 'BROKEN_HEART' => 0x1F494, // Smiley 174 + 'GROWING_HEART' => 0x1F497, // Smiley 175 + 'BEATING_HEART' => 0x1F493, // Smiley 176 + 'TWO_HEARTS' => 0x1F495, // Smiley 177 + 'SPARKLING_HEARTS' => 0x1F496, // Smiley 178 + 'REVOLVING_HEARTS' => 0x1F49E, // Smiley 179 + 'HEART_WITH_ARROW' => 0x1F498, // Smiley 180 + 'LOVE_LETTER' => 0x1F48C, // Smiley 181 + 'KISS_MARK' => 0x1F48B, // Smiley 182 + 'RING' => 0x1F48D, // Smiley 183 + 'GEM_STONE' => 0x1F48E, // Smiley 184 + 'BUST_IN_SILHOUETTE' => 0x1F464, // Smiley 185 + 'BUSTS_IN_SILHOUETTE' => 0x1F465, // Smiley 186 + 'SPEECH_BALLOON' => 0x1F4AC, // Smiley 187 + 'FOOTPRINTS' => 0x1F463, // Smiley 188 + 'THOUGHT_BALLOON' => 0x1F4AD, // Smiley 189 + 'DOG_FACE' => 0x1F436, // Smiley 190 + 'WOLF_FACE' => 0x1F43A, // Smiley 191 + 'CAT_FACE' => 0x1F431, // Smiley 192 + 'MOUSE_FACE' => 0x1F42D, // Smiley 193 + 'HAMSTER_FACE' => 0x1F439, // Smiley 194 + 'RABBIT_FACE' => 0x1F430, // Smiley 195 + 'FROG_FACE' => 0x1F438, // Smiley 196 + 'TIGER_FACE' => 0x1F42F, // Smiley 197 + 'KOALA' => 0x1F428, // Smiley 198 + 'BEAR_FACE' => 0x1F43B, // Smiley 199 + 'PIG_FACE' => 0x1F437, // Smiley 200 + 'PIG_NOSE' => 0x1F43D, // Smiley 201 + 'COW_FACE' => 0x1F42E, // Smiley 202 + 'BOAR' => 0x1F417, // Smiley 203 + 'MONKEY_FACE' => 0x1F435, // Smiley 204 + 'MONKEY' => 0x1F412, // Smiley 205 + 'HORSE_FACE' => 0x1F434, // Smiley 206 + 'SHEEP' => 0x1F411, // Smiley 207 + 'ELEPHANT' => 0x1F418, // Smiley 208 + 'PANDA_FACE' => 0x1F43C, // Smiley 209 + 'PENGUIN' => 0x1F427, // Smiley 210 + 'BIRD' => 0x1F426, // Smiley 211 + 'BABY_CHICK' => 0x1F424, // Smiley 212 + 'FRONT_FACING_BABY_CHICK' => 0x1F425, // Smiley 213 + 'HATCHING_CHICK' => 0x1F423, // Smiley 214 + 'CHICKEN' => 0x1F414, // Smiley 215 + 'SNAKE' => 0x1F40D, // Smiley 216 + 'TURTLE' => 0x1F422, // Smiley 217 + 'BUG' => 0x1F41B, // Smiley 218 + 'HONEYBEE' => 0x1F41D, // Smiley 219 + 'ANT' => 0x1F41C, // Smiley 220 + 'LADY_BEETLE' => 0x1F41E, // Smiley 221 + 'SNAIL' => 0x1F40C, // Smiley 222 + 'OCTOPUS' => 0x1F419, // Smiley 223 + 'SPIRAL_SHELL' => 0x1F41A, // Smiley 224 + 'TROPICAL_FISH' => 0x1F420, // Smiley 225 + 'FISH' => 0x1F41F, // Smiley 226 + 'DOLPHIN' => 0x1F42C, // Smiley 227 + 'SPOUTING_WHALE' => 0x1F433, // Smiley 228 + 'WHALE' => 0x1F40B, // Smiley 229 + 'COW' => 0x1F404, // Smiley 230 + 'RAM' => 0x1F40F, // Smiley 231 + 'RAT' => 0x1F400, // Smiley 232 + 'WATER_BUFFALO' => 0x1F403, // Smiley 233 + 'TIGER' => 0x1F405, // Smiley 234 + 'RABBIT' => 0x1F407, // Smiley 235 + 'DRAGON' => 0x1F409, // Smiley 236 + 'HORSE' => 0x1F40E, // Smiley 237 + 'GOAT' => 0x1F410, // Smiley 238 + 'ROOSTER' => 0x1F413, // Smiley 239 + 'DOG' => 0x1F415, // Smiley 240 + 'PIG' => 0x1F416, // Smiley 241 + 'MOUSE' => 0x1F401, // Smiley 242 + 'OX' => 0x1F402, // Smiley 243 + 'DRAGON_FACE' => 0x1F432, // Smiley 244 + 'BLOWFISH' => 0x1F421, // Smiley 245 + 'CROCODILE' => 0x1F40A, // Smiley 246 + 'BACTRIAN_CAMEL' => 0x1F42B, // Smiley 247 + 'DROMEDARY_CAMEL' => 0x1F42A, // Smiley 248 + 'LEOPARD' => 0x1F406, // Smiley 249 + 'CAT' => 0x1F408, // Smiley 250 + 'POODLE' => 0x1F429, // Smiley 251 + 'PAW_PRINTS' => 0x1F43E, // Smiley 252 + 'BOUQET' => 0x1F490, // Smiley 253 + 'CHERRY_BLOSSOM' => 0x1F338, // Smiley 254 + 'TULIP' => 0x1F337, // Smiley 255 + 'FOUR_LEAF_CLOVER' => 0x1F340, // Smiley 256 + 'ROSE' => 0x1F339, // Smiley 257 + 'SUNFLOWER' => 0x1F33B, // Smiley 258 + 'HIBISCUS' => 0x1F33A, // Smiley 259 + 'MAPLE_LEAF' => 0x1F341, // Smiley 260 + 'LEAF_FLUTTERING_IN_WIND' => 0x1F343, // Smiley 261 + 'FALLEN_LEAF' => 0x1F342, // Smiley 262 + 'HERB' => 0x1F33F, // Smiley 263 + 'EAR_OF_RICE' => 0x1F33E, // Smiley 264 + 'MUSHROOM' => 0x1F344, // Smiley 265 + 'CACTUS' => 0x1F335, // Smiley 266 + 'PALM_TREE' => 0x1F334, // Smiley 267 + 'EVERGREEN_TREE' => 0x1F332, // Smiley 268 + 'DECIDUOUS_TREE' => 0x1F333, // Smiley 269 + 'CHESTNUT' => 0x1F330, // Smiley 270 + 'SEEDLING' => 0x1F331, // Smiley 271 + 'BLOSSOM' => 0x1F33C, // Smiley 272 + 'GLOBE_WITH_MERIDIANS' => 0x1F310, // Smiley 273 + 'SUN_WITH_FACE' => 0x1F31E, // Smiley 274 + 'FULL_MOON_WITH_FACE' => 0x1F31D, // Smiley 275 + 'NEW_MOON_WITH_FACE' => 0x1F31A, // Smiley 276 + 'NEW_MOON_SYMBOL' => 0x1F311, // Smiley 277 + 'WAXING_CRESCENDING_MOON_SYMBOL' => 0x1F312, // Smiley 278 + 'FIRST_QUARTER_MOON_SYMBOL' => 0x1F313, // Smiley 279 + 'WAXING_GIBBOUS_MOON_SYMBOL' => 0x1F314, // Smiley 280 + 'FULL_MOON_SYMBOL' => 0x1F315, // Smiley 281 + 'WANNING_GIBBOUS_MOON_SYMBOL' => 0x1F316, // Smiley 282 + 'LAST_QUARTER_MOON_SYMBOL' => 0x1F317, // Smiley 283 + 'WANNING_CRESCENT_MOON_SYMBOL' => 0x1F318, // Smiley 284 + 'LAST_QUARTER_MOON_WITH_FACE' => 0x1F31C, // Smiley 285 + 'FIRST_QUARTER_MOON_WITH_FACE' => 0x1F31B, // Smiley 286 + 'CRESCENT_MOON' => 0x1F319, // Smiley 287 + 'EARTH_GLOBE_EUROPE_AFRICA' => 0x1F30D, // Smiley 288 + 'EARTH_GLOBE_AMERICAS' => 0x1F30E, // Smiley 289 + 'EARTH_GLOBE_ASIA_AUSTRALIA' => 0x1F30F, // Smiley 290 + 'VOLCANO' => 0x1F30B, // Smiley 291 + 'MILKY_WAY' => 0x1F30C, // Smiley 292 + 'SHOOTING_STAR' => 0x1F320, // Smiley 293 + 'WHITE_MEDIUM_STAR' => 0x2B50, // Smiley 294 + 'BLACK_SUN_WITH_RAYS' => 0x2600, // Smiley 295 + 'SUN_BEHIND_CLOUD' => 0x26C5, // Smiley 296 + 'CLOUD' => 0x2601, // Smiley 297 + 'HIGH_VOLTAGE_SIGN' => 0x26A1, // Smiley 298 + 'UMBRELLA_WITH_RAIN_DROPS' => 0x2614, // Smiley 299 + 'SNOW_FLAKE' => 0x2744, // Smiley 300 + 'SNOWMAN_WITHOUT_SNOW' => 0x26C4, // Smiley 301 + 'CYCLONE' => 0x1F300, // Smiley 302 + 'FOGGY' => 0x1F301, // Smiley 303 + 'RAINBOW' => 0x1F308, // Smiley 304 + 'WATER_WAVE' => 0x1F30A, // Smiley 305 + 'PINE_DECORATION' => 0x1F38D, // Smiley 306 + 'HEART_WITH_RIBBON' => 0x1F49D, // Smiley 307 + 'JAPANESE_DOLLS' => 0x1F38E, // Smiley 308 + 'SCHOOL_SATCHEL' => 0x1F392, // Smiley 309 + 'GRADUATION_CAP' => 0x1F393, // Smiley 310 + 'CARP_STREAMER' => 0x1F38F, // Smiley 311 + 'FIREWORKS' => 0x1F386, // Smiley 312 + 'FIREWORKS_SPARKLER' => 0x1F387, // Smiley 313 + 'WIND_CHIME' => 0x1F390, // Smiley 314 + 'MOON_VIEWING_CEREMONY' => 0x1F391, // Smiley 315 + 'JACK_O_LANTERN' => 0x1F383, // Smiley 316 + 'GHOST' => 0x1F47B, // Smiley 317 + 'FATHER_CHRISTMAS' => 0x1F385, // Smiley 318 + 'CHRISTMAS_TREE' => 0x1F384, // Smiley 319 + 'WRAPPED_PRESENT' => 0x1F381, // Smiley 320 + 'TANABATA_TREE' => 0x1F38B, // Smiley 321 + 'PARTY_POPPER' => 0x1F389, // Smiley 322 + 'CONFETTI_BALL' => 0x1F38A, // Smiley 323 + 'BALLOON' => 0x1F388, // Smiley 324 + 'CROSSED_FLAGS' => 0x1F38C, // Smiley 325 + 'CRYSTAL_BALL' => 0x1F52E, // Smiley 326 + 'MOVIE_CAMERA' => 0x1F3A5, // Smiley 327 + 'CAMERA' => 0x1F4F7, // Smiley 328 + 'VIDEO_CAMERA' => 0x1F4F9, // Smiley 329 + 'VIDEOCASSETTE' => 0x1F4FC, // Smiley 330 + 'OPTICAL_DISC' => 0x1F4BF, // Smiley 331 + 'DVD' => 0x1F4C0, // Smiley 332 + 'MINIDISC' => 0x1F4BD, // Smiley 333 + 'FLOPPY_DISK' => 0x1F4BE, // Smiley 334 + 'PERSONAL_COMPUTER' => 0x1F4BB, // Smiley 335 + 'MOBILE_PHONE' => 0x1F4F1, // Smiley 336 + 'BLACK_TELEPHONE' => 0x260E, // Smiley 337 + 'TELEPHONE_RECEIVER' => 0x1F4DE, // Smiley 338 + 'PAGER' => 0x1F4DF, // Smiley 339 + 'FAX_MACHINE' => 0x1F4E0, // Smiley 340 + 'SATELLITE_ANTENNA' => 0x1F4E1, // Smiley 341 + 'TELEVISION' => 0x1F4FA, // Smiley 342 + 'RADIO' => 0x1F4FB, // Smiley 343 + 'SPEAKER_WITH_THREE_SOUND_WAVES' => 0x1F50A, // Smiley 344 + 'SPEAKER_WITH_ONE_SOUND_WAVE' => 0x1F509, // Smiley 345 + 'SPEAKER' => 0x1F508, // Smiley 346 + 'SPEAKER_WITH_CANCELLATION_STROKE' => 0x1F507, // Smiley 347 + 'BELL' => 0x1F514, // Smiley 348 + 'BELL_WITH_CANCELLATION_STROKE' => 0x1F515, // Smiley 349 + 'PUBLIC_ADDRESS_LOUDSPEAKER' => 0x1F4E2, // Smiley 350 + 'CHEERING_MEGAPHONE' => 0x1F4E3, // Smiley 351 + 'HOURGLASS_WITH_FLOWING_SAND' => 0x23F3, // Smiley 352 + 'HOURGLASS' => 0x231B, // Smiley 353 + 'ALARM_CLOCK' => 0x23F0, // Smiley 354 + 'WATCH' => 0x231A, // Smiley 355 + 'OPEN_LOCK' => 0x1F513, // Smiley 356 + 'LOCK' => 0x1F512, // Smiley 357 + 'LOCK_WITH_INK_PEN' => 0x1F50F, // Smiley 358 + 'CLOSED_LOCK_WITH_KEY' => 0x1F510, // Smiley 359 + 'KEY' => 0x1F511, // Smiley 360 + 'RIGHT_POINTING_MAGNIFYING_GLASS' => 0x1F50E, // Smiley 361 + 'ELECTRIC_LIGHT_BULB' => 0x1F4A1, // Smiley 362 + 'ELECTRIC_TORCH' => 0x1F526, // Smiley 363 + 'HIGH_BRIGHTNESS_SYMBOL' => 0x1F506, // Smiley 364 + 'LOW_BRIGHTNESS_SYMBOL' => 0x1F505, // Smiley 365 + 'ELECTRIC_PLUG' => 0x1F50C, // Smiley 366 + 'BATTERY' => 0x1F50B, // Smiley 367 + 'LEFT_POINTING_MAGNIFYING_GLASS' => 0x1F50D, // Smiley 368 + 'BATHTUB' => 0x1F6C1, // Smiley 369 + 'BATH' => 0x1F6C0, // Smiley 370 + 'SHOWER' => 0x1F6BF, // Smiley 371 + 'TOILET' => 0x1F6BD, // Smiley 372 + 'WRENCH' => 0x1F527, // Smiley 373 + 'NUT_AND_BOLT' => 0x1F529, // Smiley 374 + 'HAMMER' => 0x1F528, // Smiley 375 + 'DOOR' => 0x1F6AA, // Smiley 376 + 'SMOKING_SYMBOL' => 0x1F6AC, // Smiley 377 + 'BOMB' => 0x1F4A3, // Smiley 378 + 'PISTOL' => 0x1F52B, // Smiley 379 + 'HOCHO' => 0x1F52A, // Smiley 380 + 'PILL' => 0x1F48A, // Smiley 381 + 'SYRINGE' => 0x1F489, // Smiley 382 + 'MONEY_BAG' => 0x1F4B0, // Smiley 383 + 'BANKNOTE_WITH_YEN_SIGN' => 0x1F4B4, // Smiley 384 + 'BANKNOTE_WITH_DOLLAR_SIGN' => 0x1F4B5, // Smiley 385 + 'BANKNOTE_WITH_POUND_SIGN' => 0x1F4B7, // Smiley 386 + 'BANKNOTE_WITH_EURO_SIGN' => 0x1F4B6, // Smiley 387 + 'CREDIT_CARD' => 0x1F4B3, // Smiley 388 + 'MONEY_WITH_WINGS' => 0x1F4B8, // Smiley 389 + 'MOBILE_PHONE_WITH_RIGHTWARDS_ARROW' => 0x1F4F2, // Smiley 390 + 'E_MAIL_SYMBOL' => 0x1F4E7, // Smiley 391 + 'INBOX_TRAY' => 0x1F4E5, // Smiley 392 + 'OUTBOX_TRAY' => 0x1F4E4, // Smiley 393 + 'ENVELOPE' => 0x2709, // Smiley 394 + 'ENVELOPE_WITH_DOWNWARDS_ARROW_ABOVE' => 0x1F4E9, // Smiley 395 + 'INCOMING_ENVELOPE' => 0x1F4E8, // Smiley 396 + 'POSTAL_HORN' => 0x1F4EF, // Smiley 397 + 'CLOSED_MAILBOX_WITH_RAISED_FLAG' => 0x1F4EB, // Smiley 398 + 'CLOSED_MAILBOX_WITH_LOWERED_FLAG' => 0x1F4EA, // Smiley 399 + 'OPEN_MAILBOX_WITH_RAISED_FLAG' => 0x1F4EC, // Smiley 400 + 'OPEN_MAILBOX_WITH_LOWERED_FLAG' => 0x1F4ED, // Smiley 401 + 'POSTBOX' => 0x1F4EE, // Smiley 402 + 'PACKAGE' => 0x1F4E6, // Smiley 403 + 'MEMO' => 0x1F4DD, // Smiley 404 + 'PAGE_FACING_UP' => 0x1F4C4, // Smiley 405 + 'PAGE_WITH_CURL' => 0x1F4C3, // Smiley 406 + 'BOOKMARK_TABS' => 0x1F4D1, // Smiley 407 + 'BAR_CHART' => 0x1F4CA, // Smiley 408 + 'CHART_WITH_UPWARDS_TREND' => 0x1F4C8, // Smiley 409 + 'CHART_WITH_DOWNWARD_TREND' => 0x1F4C9, // Smiley 410 + 'SCROLL' => 0x1F4DC, // Smiley 411 + 'CLIPBOARD' => 0x1F4CB, // Smiley 412 + 'CALENDAR' => 0x1F4C5, // Smiley 413 + 'TEAR_OFF_CALENDAR' => 0x1F4C6, // Smiley 414 + 'CARD_INDEX' => 0x1F4C7, // Smiley 415 + 'FILE_FOLDER' => 0x1F4C1, // Smiley 416 + 'OPEN_FILE_FOLDER' => 0x1F4C2, // Smiley 417 + 'BLACK_SCISSORS' => 0x2702, // Smiley 418 + 'PUSHPIN' => 0x1F4CC, // Smiley 419 + 'PAPERCLIP' => 0x1F4CE, // Smiley 420 + 'BLACK_NIB' => 0x2712, // Smiley 421 + 'PENCIL' => 0x270F, // Smiley 422 + 'STRAIGHT_RULER' => 0x1F4CF, // Smiley 423 + 'TRIANGULAR_RULER' => 0x1F4D0, // Smiley 424 + 'CLOSED_BOOK' => 0x1F4D5, // Smiley 425 + 'GREEN_BOOK' => 0x1F4D7, // Smiley 426 + 'BLUE_BOOK' => 0x1F4D8, // Smiley 427 + 'ORANGE_BOOK' => 0x1F4D9, // Smiley 428 + 'NOTEBOOK' => 0x1F4D3, // Smiley 429 + 'NOTEBOOK_WITH_DECORATIVE_COVER' => 0x1F4D4, // Smiley 430 + 'LEDGER' => 0x1F4D2, // Smiley 431 + 'BOOKS' => 0x1F4DA, // Smiley 432 + 'OPEN_BOOK' => 0x1F4D6, // Smiley 433 + 'BOOKMARK' => 0x1F516, // Smiley 434 + 'NAME_BADGE' => 0x1F4DB, // Smiley 435 + 'MICROSCOPE' => 0x1F52C, // Smiley 436 + 'TELESCOPE' => 0x1F52D, // Smiley 437 + 'NEWSPAPER' => 0x1F4F0, // Smiley 438 + 'ARTIST_PALETTE' => 0x1F3A8, // Smiley 439 + 'CLAPPER_BOARD' => 0x1F3AC, // Smiley 440 + 'MICROPHONE' => 0x1F3A4, // Smiley 441 + 'HEADPHONE' => 0x1F3A7, // Smiley 442 + 'MUSICAL_SCORE' => 0x1F3BC, // Smiley 443 + 'MUSICAL_NOTE' => 0x1F3B5, // Smiley 444 + 'MULTIPLE_MUSICAL_NOTES' => 0x1F3B6, // Smiley 445 + 'MUSICAL_KEYBOARD' => 0x1F3B9, // Smiley 446 + 'VIOLIN' => 0x1F3BB, // Smiley 447 + 'TRUMPET' => 0x1F3BA, // Smiley 448 + 'SAXOPHONE' => 0x1F3B7, // Smiley 449 + 'GUITAR' => 0x1F3B8, // Smiley 450 + 'ALIEN_MONSTER' => 0x1F47E, // Smiley 451 + 'VIDEO_GAME' => 0x1F3AE, // Smiley 452 + 'PLAYING_CARD_BLACK_JOKER' => 0x1F0CF, // Smiley 453 + 'FLOWER_PLAYING_CARDS' => 0x1F3B4, // Smiley 454 + 'MAHJONG_TILE_RED_DRAGON' => 0x1F004, // Smiley 455 + 'GAME_DIE' => 0x1F3B2, // Smiley 456 + 'DIRECT_HIT' => 0x1F3AF, // Smiley 457 + 'AMERICAN_FOOTBALL' => 0x1F3C8, // Smiley 458 + 'BASKETBALL_AND_HOOP' => 0x1F3C0, // Smiley 459 + 'SOCCER_BALL' => 0x26BD, // Smiley 460 + 'BASEBALL' => 0x26BE, // Smiley 461 + 'GREEN_BOOK' => 0x1F4D7, // Smiley 462 + 'TENNIS_RACQUET_AND_BALL' => 0x1F3BE, // Smiley 463 + 'BILLIARDS' => 0x1F3B1, // Smiley 464 + 'RUGBY_FOOTBALL' => 0x1F3C9, // Smiley 465 + 'BOWLING' => 0x1F3B3, // Smiley 466 + 'FLAG_IN_HOLE' => 0x26F3, // Smiley 467 + 'MOUNTAIN_BYCICLIST' => 0x1F6B5, // Smiley 468 + 'BYCICLIST' => 0x1F6B4, // Smiley 469 + 'CHECKERED_FLAG' => 0x1F3C1, // Smiley 470 + 'HORSE_RACING' => 0x1F3C7, // Smiley 471 + 'TROPHY' => 0x1F3C6, // Smiley 472 + 'SKI_AND_SKI_BOOT' => 0x1F3BF, // Smiley 473 + 'SNOWBOARDER' => 0x1F3C2, // Smiley 474 + 'SWIMMER' => 0x1F3CA, // Smiley 475 + 'SURFER' => 0x1F3C4, // Smiley 476 + 'FISHING_POLE_AND_FISH' => 0x1F3A3, // Smiley 477 + 'HOT_BEVERAGE' => 0x2615, // Smiley 478 + 'TEACUP_WITHOUT_HANDLE' => 0x1F375, // Smiley 479 + 'SAKE_BOTTLE_AND_CUP' => 0x1F376, // Smiley 480 + 'BABY_BOTTLE' => 0x1F37C, // Smiley 481 + 'BEER_MUG' => 0x1F37A, // Smiley 482 + 'CLINKING_BEER_MUGS' => 0x1F37B, // Smiley 483 + 'TROPICAL_DRINK' => 0x1F379, // Smiley 484 + 'WINE_GLASS' => 0x1F377, // Smiley 485 + 'FORK_AND_KNIFE' => 0x1F374, // Smiley 486 + 'SLICE_OF_PIZZA' => 0x1F355, // Smiley 487 + 'HAMBURGER' => 0x1F354, // Smiley 488 + 'FRENCH_FRIES' => 0x1F35F, // Smiley 489 + 'POULTRY_LEG' => 0x1F357, // Smiley 490 + 'MEAT_ON_BONE' => 0x1F356, // Smiley 491 + 'SPAGHETTI' => 0x1F35D, // Smiley 492 + 'CURRY_AND_RICE' => 0x1F35B, // Smiley 493 + 'FRIED_SHRIMP' => 0x1F364, // Smiley 494 + 'BENTO_BOX' => 0x1F371, // Smiley 495 + 'SUSHI' => 0x1F363, // Smiley 496 + 'FISH_CAKE_WITH_SWIRL_DESIGN' => 0x1F365, // Smiley 497 + 'RICE_BALL' => 0x1F359, // Smiley 498 + 'RICE_CRACKER' => 0x1F358, // Smiley 499 + 'COOKED_RICE' => 0x1F35A, // Smiley 500 + 'STEAMING_BOWL' => 0x1F35C, // Smiley 501 + 'POT_OF_FOOD' => 0x1F372, // Smiley 502 + 'ODEN' => 0x1F362, // Smiley 503 + 'DANGO' => 0x1F361, // Smiley 504 + 'COOKING' => 0x1F373, // Smiley 505 + 'BREAD' => 0x1F35E, // Smiley 506 + 'DOUGHNUT' => 0x1F369, // Smiley 507 + 'CUSTARD' => 0x1F36E, // Smiley 508 + 'SOFT_ICE_CREAM' => 0x1F366, // Smiley 509 + 'ICE_CREAM' => 0x1F368, // Smiley 510 + 'SHAVED_ICE' => 0x1F367, // Smiley 511 + 'BIRTHDAY_CAKE' => 0x1F382, // Smiley 512 + 'SHORTCAKE' => 0x1F370, // Smiley 513 + 'COOKIE' => 0x1F36A, // Smiley 514 + 'CHOCOLATE_BAR' => 0x1F36B, // Smiley 515 + 'CANDY' => 0x1F36C, // Smiley 516 + 'LOLLIPOP' => 0x1F36D, // Smiley 517 + 'HONEY_POT' => 0x1F36F, // Smiley 518 + 'RED_APPLE' => 0x1F34E, // Smiley 519 + 'GREEN_APPLE' => 0x1F34F, // Smiley 520 + 'TANGERINE' => 0x1F34A, // Smiley 521 + 'LEMON' => 0x1F34B, // Smiley 522 + 'CHERRIES' => 0x1F352, // Smiley 523 + 'GRAPES' => 0x1F347, // Smiley 524 + 'WATERMELON' => 0x1F349, // Smiley 525 + 'STRAWBERRY' => 0x1F353, // Smiley 526 + 'PEACH' => 0x1F351, // Smiley 527 + 'MELON' => 0x1F348, // Smiley 528 + 'BANANA' => 0x1F34C, // Smiley 529 + 'PEAR' => 0x1F350, // Smiley 530 + 'PINEAPPLE' => 0x1F34D, // Smiley 531 + 'ROASTED_SWEET_POTATO' => 0x1F360, // Smiley 532 + 'AUBERGINE' => 0x1F346, // Smiley 533 + 'TOMATO' => 0x1F345, // Smiley 534 + 'EAR_OF_MAIZE' => 0x1F33D, // Smiley 535 + 'HOUSE_BUILDING' => 0x1F3E0, // Smiley 536 + 'HOUSE_WITH_GARDEN' => 0x1F3E1, // Smiley 537 + 'SCHOOL' => 0x1F3EB, // Smiley 538 + 'OFFICE_BUILDING' => 0x1F3E2, // Smiley 539 + 'JAPANESE_POST_OFFICE' => 0x1F3E3, // Smiley 540 + 'HOSPITAL' => 0x1F3E5, // Smiley 541 + 'BANK' => 0x1F3E6, // Smiley 542 + 'CONVINIENCE_STORE' => 0x1F3EA, // Smiley 543 + 'LOVE_HOTEL' => 0x1F3E9, // Smiley 544 + 'HOTEL' => 0x1F3E8, // Smiley 545 + 'WEDDING' => 0x1F492, // Smiley 546 + 'CHURCH' => 0x26EA, // Smiley 547 + 'DEPARTMENT_STORE' => 0x1F3EC, // Smiley 548 + 'EUROPEAN_POST_OFFICE' => 0x1F3E4, // Smiley 549 + 'SUNSET_OVER_BUILDINGS' => 0x1F307, // Smiley 550 + 'CITYSCAPE_AT_DUSK' => 0x1F306, // Smiley 551 + 'JAPANESE_CASTLE' => 0x1F3EF, // Smiley 552 + 'EUROPEAN_CASTLE' => 0x1F3F0, // Smiley 553 + 'TENT' => 0x26FA, // Smiley 554 + 'FACTORY' => 0x1F3ED, // Smiley 555 + 'TOKYO_TOWER' => 0x1F5FC, // Smiley 556 + 'SILHOUETTE_OF_JAPAN' => 0x1F5FE, // Smiley 557 + 'MOUNT_FUJI' => 0x1F5FB, // Smiley 558 + 'SUNRISE_OVER_MOUNTAINS' => 0x1F304, // Smiley 559 + 'SUNRISE' => 0x1F305, // Smiley 560 + 'NIGHT_WITH_STARS' => 0x1F303, // Smiley 561 + 'STATUE_OF_LIBERTY' => 0x1F5FD, // Smiley 562 + 'BRIDGE_AT_NIGHT' => 0x1F309, // Smiley 563 + 'CAROUSEL_HORSE' => 0x1F3A0, // Smiley 564 + 'FERRIS_WHEEL' => 0x1F3A1, // Smiley 565 + 'FOUNTAIN' => 0x26F2, // Smiley 566 + 'ROLLER_COASTER' => 0x1F3A2, // Smiley 567 + 'SHIP' => 0x1F6A2, // Smiley 568 + 'SAILBOAT' => 0x26F5, // Smiley 569 + 'SPEEDBOAT' => 0x1F6A4, // Smiley 570 + 'ROWBOAT' => 0x1F6A3, // Smiley 571 + 'ANCHOR' => 0x2693, // Smiley 572 + 'ROCKET' => 0x1F680, // Smiley 573 + 'AIRPLANE' => 0x2708, // Smiley 574 + 'SEAT' => 0x1F4BA, // Smiley 575 + 'HELICOPTER' => 0x1F681, // Smiley 576 + 'STEAM_LOCOMOTIVE' => 0x1F682, // Smiley 577 + 'TRAM' => 0x1F68A, // Smiley 578 + 'STATION' => 0x1F689, // Smiley 579 + 'MOUNTAIN_RAILWAY' => 0x1F69E, // Smiley 580 + 'TRAIN' => 0x1F686, // Smiley 581 + 'HIGH_SPEED_TRAIN' => 0x1F684, // Smiley 582 + 'HIGH_SPEED_TRAIN_WITH_BULLET_NOSE' => 0x1F685, // Smiley 583 + 'LIGHT_RAIL' => 0x1F688, // Smiley 584 + 'METRO' => 0x1F687, // Smiley 585 + 'MONO_RAIL' => 0x1F69D, // Smiley 586 + 'TRAM_CAR' => 0x1F68B, // Smiley 587 + 'RAILWAY_CAR' => 0x1F683, // Smiley 588 + 'TROLLEYBUS' => 0x1F68E, // Smiley 589 + 'BUS' => 0x1F68C, // Smiley 590 + 'ONCOMING_BUS' => 0x1F68D, // Smiley 591 + 'RECREATION_VEHICLE' => 0x1F699, // Smiley 592 + 'ONCOMING_AUTOMOBILE' => 0x1F698, // Smiley 593 + 'AUTOMOBILE' => 0x1F697, // Smiley 594 + 'TAXI' => 0x1F695, // Smiley 595 + 'ONCOMING_TAXI' => 0x1F696, // Smiley 596 + 'ARTICULATED_LORRY' => 0x1F69B, // Smiley 597 + 'DELIVERY_TRUCK' => 0x1F69A, // Smiley 598 + 'POLICE_CARS_REVOLVING_LIGHT' => 0x1F6A8, // Smiley 599 + 'POLICE_CAR' => 0x1F693, // Smiley 600 + 'ONCOMING_POLICE_CAR' => 0x1F694, // Smiley 601 + 'FIRE_ENGINE' => 0x1F692, // Smiley 602 + 'AMBULANCE' => 0x1F691, // Smiley 603 + 'MINBUS' => 0x1F690, // Smiley 604 + 'BICYCLE' => 0x1F6B2, // Smiley 605 + 'AERIAL_TRAMWAY' => 0x1F6A1, // Smiley 606 + 'SUSPENSION_RAILWAY' => 0x1F69F, // Smiley 607 + 'MOUNTAIN_CABLEWAY' => 0x1F6A0, // Smiley 608 + 'TRACTOR' => 0x1F69C, // Smiley 609 + 'BARBER_POLE' => 0x1F488, // Smiley 610 + 'BUS_STOP' => 0x1F68F, // Smiley 611 + 'TICKET' => 0x1F3AB, // Smiley 612 + 'VERTICAL_TRAFFIC_LIGHT' => 0x1F6A6, // Smiley 613 + 'HORIZONTAL_TRAFFIC_LIGHT' => 0x1F6A5, // Smiley 614 + 'WARNING_SIGN' => 0x26A0, // Smiley 615 + 'CONSTRUCTION_SIGN' => 0x1F6A7, // Smiley 616 + 'JAPANESE_SYMBOL_FOR_BEGINNER' => 0x1F530, // Smiley 617 + 'FUEL_PUMP' => 0x26FD, // Smiley 618 + 'IZAKAYA_LANTERN' => 0x1F3EE, // Smiley 619 + 'SLOT_MACHINE' => 0x1F3B0, // Smiley 620 + 'HOT_SPRINGS' => 0x2668, // Smiley 621 + 'MOYAI' => 0x1F5FF, // Smiley 622 + 'CIRCUS_TENT' => 0x1F3AA, // Smiley 623 + 'PERFORMING_ARTS' => 0x1F3AD, // Smiley 624 + 'ROUND_PUSHPIN' => 0x1F4CD, // Smiley 625 + 'TRIANGULAR_FLAG_ON_POST' => 0x1F6A9, // Smiley 626 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_JP' => [0xD83C, 0xDDEF, 0xD83C, 0xDDF5], // Smiley 627 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_KR' => [0xD83C, 0xDDF0, 0xD83C, 0xDDF7], // Smiley 628 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_DE' => [0xD83C, 0xDDE9, 0xD83C, 0xDDEA], // Smiley 629 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_CN' => [0xD83C, 0xDDE8, 0xD83C, 0xDDF3], // Smiley 630 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_US' => [0xD83C, 0xDDFA, 0xD83C, 0xDDF8], // Smiley 631 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_FR' => [0xD83C, 0xDDEB, 0xD83C, 0xDDF7], // Smiley 632 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_ES' => [0xD83C, 0xDDEA, 0xD83C, 0xDDF8], // Smiley 633 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_IT' => [0xD83C, 0xDDEE, 0xD83C, 0xDDF9], // Smiley 634 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_RU' => [0xD83C, 0xDDF7, 0xD83C, 0xDDFA], // Smiley 635 + 'REGIONAL_INDICATOR_SYMBOL_LETTERS_GB' => [0xD83C, 0xDDEC, 0xD83C, 0xDDE7], // Smiley 636 + 'KEYCAP_1' => [0x0031, 0x20E3], // Smiley 637 + 'KEYCAP_2' => [0x0032, 0x20E3], // Smiley 638 + 'KEYCAP_3' => [0x0033, 0x20E3], // Smiley 639 + 'KEYCAP_4' => [0x0034, 0x20E3], // Smiley 640 + 'KEYCAP_5' => [0x0035, 0x20E3], // Smiley 641 + 'KEYCAP_6' => [0x0036, 0x20E3], // Smiley 642 + 'KEYCAP_7' => [0x0037, 0x20E3], // Smiley 643 + 'KEYCAP_8' => [0x0038, 0x20E3], // Smiley 644 + 'KEYCAP_9' => [0x0039, 0x20E3], // Smiley 645 + 'KEYCAP_0' => [0x0030, 0x20E3], // Smiley 646 + 'KEYCAP_10' => 0x1F51F, // Smiley 647 + 'INPUT_SYMBOL_FOR_NUMBERS' => 0x1F522, // Smiley 648 + 'HASH_KEY' => [0x0023, 0x20E3], // Smiley 649 + 'INPUT_SYMBOL_FOR_SYMBOLS' => 0x1F523, // Smiley 650 + 'UPWARDS_BLACK_ARROW' => 0x2B06, // Smiley 651 + 'DOWNWARDS_BLACK_ARROW' => 0x2B07, // Smiley 652 + 'LEFTWARDS_BLACK_ARROW' => 0x2B05, // Smiley 653 + 'BLACK_RIGHTWARDS_ARROW' => 0x27A1, // Smiley 654 + 'INPUT_SYMBOL_FOR_LATIN_CAPITAL_LETTERS' => 0x1F520, // Smiley 655 + 'INPUT_SYMBOL_FOR_LATIN_SMALL_LETTERS' => 0x1F521, // Smiley 656 + 'INPUT_SYMBOL_FOR_LATIN_LETTERS' => 0x1F524, // Smiley 657 + 'NORTH_EAST_ARROW' => 0x2197, // Smiley 658 + 'NORTH_WEST_ARROW' => 0x2196, // Smiley 659 + 'SOUTH_EAST_ARROW' => 0x2198, // Smiley 660 + 'SOUTH_WEST_ARROW' => 0x2199, // Smiley 661 + 'LEFT_RIGHT_ARROW' => 0x2194, // Smiley 662 + 'UP_DOWN_ARROW' => 0x2195, // Smiley 663 + 'ANTICLOCKWISE_DOWNWARDS_AND_UPWARDS_OPEN_CIRCLE_ARROWS' => 0x1F504, // Smiley 664 + 'BLACK_LEFT_POINTING_TRIANGLE' => 0x25C0, // Smiley 665 + 'BLACK_RIGHT_POINTING_TRIANGLE' => 0x25B6, // Smiley 666 + 'UP_POINTING_SMALL_RED_TRIANGLE' => 0x1F53C, // Smiley 667 + 'DOWN_POINTING_SMALL_RED_TRIANGLE' => 0x1F53D, // Smiley 668 + 'LEFTWARDS_ARROW_WITH_HOOK' => 0x21A9, // Smiley 669 + 'RIGHTWARDS_ARROW_WITH_HOOK' => 0x21AA, // Smiley 670 + 'INFORMATION_SOURCE' => 0x2139, // Smiley 671 + 'BLACK_LEFT_POINTING_DOUBLE_TRIANGLE' => 0x23EA, // Smiley 672 + 'BLACK_RIGHT_POINTING_DOUBLE_TRIANGLE' => 0x23E9, // Smiley 673 + 'BLACK_UP_POINTING_DOUBLE_TRIANGLE' => 0x23EB, // Smiley 674 + 'BLACK_DOWN_POINTING_DOUBLE_TRIANGLE' => 0x23EC, // Smiley 675 + 'ARROW_POINTING_RIGHTWARDS_THEN_CURVING_DOWNWARDS' => 0x2935, // Smiley 676 + 'ARROW_POINTING_RIGHTWARDS_THEN_CURVING_UPWARDS' => 0x2934, // Smiley 677 + 'SQUARED_OK' => 0x1F197, // Smiley 678 + 'TWISTED_RIGHTWARDS_ARROWS' => 0x1F500, // Smiley 679 + 'CLOCKWISE_RIGHTWARDS_AND_LEFTWARDS_OPEN_CIRCLE_ARROWS' => 0x1F501, // Smiley 680 + 'CLOCKWISE_RIGHTWARDS_AND_LEFTWARDS_ARTOWS_WITH_CIRCLED_ONE_OVERLAY' => 0x1F502, // Smiley 681 + 'SQUARED_NEW' => 0x1F195, // Smiley 682 + 'SQUARED_UP_WITH_EXCLAMATION_MARK' => 0x1F199, // Smiley 683 + 'SQUARED_COOL' => 0x1F192, // Smiley 684 + 'SQUARED_FREE' => 0x1F193, // Smiley 685 + 'SQUARED_NG' => 0x1F196, // Smiley 686 + 'ANTENNA_WITH_BARS' => 0x1F4F6, // Smiley 687 + 'CINEMA' => 0x1F3A6, // Smiley 688 + 'SQUARED_KATAKANA_KOKO' => 0x1F201, // Smiley 689 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_6307' => 0x1F22F, // Smiley 690 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_7A7A' => 0x1F233, // Smiley 691 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_6E80' => 0x1F235, // Smiley 692 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_5408' => 0x1F234, // Smiley 693 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_7981' => 0x1F232, // Smiley 694 + 'CIRCLED_IDEOGRAPH_ADVANTAGE' => 0x1F250, // Smiley 695 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_5272' => 0x1F239, // Smiley 696 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_55B6' => 0x1F23A, // Smiley 697 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_6709' => 0x1F236, // Smiley 698 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_7121' => 0x1F21A, // Smiley 699 + 'RESTROOM' => 0x1F6BB, // Smiley 700 + 'MENS_SYMBOL' => 0x1F6B9, // Smiley 701 + 'WOMENS_SYMBOL' => 0x1F6BA, // Smiley 702 + 'BABY_SYMBOL' => 0x1F6BC, // Smiley 703 + 'WATER_CLOSET' => 0x1F6BE, // Smiley 704 + 'POTABLE_WATER_SYMBOL' => 0x1F6B0, // Smiley 705 + 'PUT_LITTER_IN_ITS_PLACE_SYMBOL' => 0x1F6AE, // Smiley 706 + 'NEGATIVE_SQUARED_LATIN_CAPITAL_LETTER_P' => 0x1F17F, // Smiley 707 + 'WHEELCHAIR_SYMBOL' => 0x267F, // Smiley 708 + 'NO_SMOKING_SYMBOL' => 0x1F6AD, // Smiley 709 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_6708' => 0x1F237, // Smiley 710 + 'SQUARED_CJK_UNIFIED_IDEOGRAPH_6533' => 0x1F238, // Smiley 711 + 'SQUARED_KATAKANA_SA' => 0x1F202, // Smiley 712 + 'CIRCLED_LATIN_CAPITAL_LETTER_M' => 0x24C2, // Smiley 713 + 'PASSPORT_CONTROL' => 0x1F6C2, // Smiley 714 + 'BAGGAGE_CLAIM' => 0x1F6C4, // Smiley 715 + 'LEFT_LUGGAGE' => 0x1F6C5, // Smiley 716 + 'CUSTOMS' => 0x1F6C3, // Smiley 717 + 'CIRCLED_IDEOGRAPH_ACCEPT' => 0x1F251, // Smiley 718 + 'CIRCLED_IDEOGRAPH_SECRET' => 0x3299, // Smiley 719 + 'CIRCLED_IDEOGRAPH_CONGRATULATIONS' => 0x3297, // Smiley 720 + 'SQUARED_CL' => 0x1F191, // Smiley 721 + 'SQUARED_SOS' => 0x1F198, // Smiley 722 + 'SQUARED_ID' => 0x1F194, // Smiley 723 + 'NO_ENTRY_SIGN' => 0x1F6AB, // Smiley 724 + 'NO_ONE_UNDER_EIGHTEEN_SYMBOL' => 0x1F51E, // Smiley 725 + 'NO_MOBILE_PHONES' => 0x1F4F5, // Smiley 726 + 'DO_NOT_LITTER_SYMBOL' => 0x1F6AF, // Smiley 727 + 'NON_POTABLE_WATER_SYMBOL' => 0x1F6B1, // Smiley 728 + 'NO_BICYCLES' => 0x1F6B3, // Smiley 729 + 'NO_PEDESTRIANS' => 0x1F6B7, // Smiley 730 + 'CHILDREN_CROSSING' => 0x1F6B8, // Smiley 731 + 'NO_ENTRY' => 0x26D4, // Smiley 732 + 'EIGHT_SPOKED_ASTERISK' => 0x2733, // Smiley 733 + 'SPARKLE' => 0x2747, // Smiley 734 + 'NEGATIVE_SQUARED_CROSS_MARK' => 0x274E, // Smiley 735 + 'WHITE_HEAVY_CHECK_MARK' => 0x2705, // Smiley 736 + 'EIGHT_POINTED_BLACK_STAR' => 0x2734, // Smiley 737 + 'HEART_DECORATION' => 0x1F49F, // Smiley 738 + 'SQUARED_VS' => 0x1F19A, // Smiley 739 + 'VIBRATION_MODE' => 0x1F4F3, // Smiley 740 + 'MOBILE_PHONE_OFF' => 0x1F4F4, // Smiley 741 + 'NEGATIVE_SQUARED_LATIN_CAPITAL_LETTER_A' => 0x1F170, // Smiley 742 + 'NEGATIVE_SQUARED_LATIN_CAPITAL_LETTER_B' => 0x1F171, // Smiley 743 + 'NEGATIVE_SQUARED_AB' => 0x1F18E, // Smiley 744 + 'NEGATIVE_SQUARED_LATIN_CAPITAL_LETTER_O' => 0x1F17E, // Smiley 745 + 'DIAMOND_SHAPE_WITH_A_DOT_INSIDE' => 0x1F4A0, // Smiley 746 + 'DOUBLE_CURLY_LOOP' => 0x27BF, // Smiley 747 + 'BLACK_UNIVERSAL_RECYCLING_SYMBOL' => 0x267B, // Smiley 748 + 'ARIES' => 0x2648, // Smiley 749 + 'TAURUS' => 0x2649, // Smiley 750 + 'GEMINI' => 0x264A, // Smiley 751 + 'CANCER' => 0x264B, // Smiley 752 + 'LEO' => 0x264C, // Smiley 753 + 'VIRGO' => 0x264D, // Smiley 754 + 'LIBRA' => 0x264E, // Smiley 755 + 'SCORPIUS' => 0x264F, // Smiley 756 + 'SAGITTARIUS' => 0x2650, // Smiley 757 + 'CAPRICORN' => 0x2651, // Smiley 758 + 'AQUARIUS' => 0x2652, // Smiley 759 + 'PISCES' => 0x2653, // Smiley 760 + 'OPHIUCHUS' => 0x26CE, // Smiley 761 + 'SIX_POINTED_STAR_WITH_MIDDLE_DOT' => 0x1F52F, // Smiley 762 + 'AUTOMATED_TELLER_MACHINE' => 0x1F3E7, // Smiley 763 + 'CHART_WITH_UPWARDS_TREND_AND_YEN_SIGN' => 0x1F4B9, // Smiley 764 + 'HEAVY_DOLLAR_SIGN' => 0x1F4B2, // Smiley 765 + 'CURRENCY_EXCHANGE' => 0x1F4B1, // Smiley 766 + 'COPYRIGHT_SIGN' => 0x00A9, // Smiley 767 + 'REGISTERED_SIGN' => 0x00AE, // Smiley 768 + 'TRADEMARK_SIGN' => 0x2122, // Smiley 769 + 'CROSS_MARK' => 0x274C, // Smiley 770 + 'DOUBLE_EXCLAMATION_MARK' => 0x203C, // Smiley 771 + 'EXCLAMATION_QUESTION_MARK' => 0x2049, // Smiley 772 + 'HEAVY_EXCLAMATION_MARK_SYMBOL' => 0x2757, // Smiley 773 + 'BLACK_QUESTION_MARK_ORNAMENT' => 0x2753, // Smiley 774 + 'WHITE_EXCLAMATION_MARK_ORNAMENT' => 0x2755, // Smiley 775 + 'WHITE_QUESTION_MARK_ORNAMENT' => 0x2754, // Smiley 776 + 'HEAVY_LARGE_CIRCLE' => 0x2B55, // Smiley 777 + 'TOP_WITH_UPWARDS_ARROW_ABOVE' => 0x1F51D, // Smiley 778 + 'END_WITH_LEFTWARDS_ARROW_ABOVE' => 0x1F51A, // Smiley 779 + 'BACK_WITH_LEFTWARDS_ARROW_ABOVE' => 0x1F519, // Smiley 780 + 'ON_WITH_EXCLAMATION_MARK_WITH_LEFT_RIGHT_ARROW_ABOVE' => 0x1F51B, // Smiley 781 + 'SOON_WITH_RIGHTWARDS_ARROW_ABOVE' => 0x1F51C, // Smiley 782 + 'CLOCKWISE_DOWNWARDS_AND_UPWARDS_OPEN_CIRCLE_ARROWS' => 0x1F503, // Smiley 783 + 'CLOCK_FACE_TWELVE_OCLOCK' => 0x1F55B, // Smiley 784 + 'CLOCK_FACE_TWELVE_THIRTY' => 0x1F567, // Smiley 785 + 'CLOCK_FACE_ONE_OCLOCK' => 0x1F550, // Smiley 786 + 'CLOCK_FACE_ONE_THIRTY' => 0x1F55C, // Smiley 787 + 'CLOCK_FACE_TWO_OCLOCK' => 0x1F551, // Smiley 788 + 'CLOCK_FACE_TWO_THIRTY' => 0x1F55D, // Smiley 789 + 'CLOCK_FACE_THREE_OCLOCK' => 0x1F552, // Smiley 790 + 'CLOCK_FACE_THREE_THIRTY' => 0x1F55E, // Smiley 791 + 'CLOCK_FACE_FOUR_OCLOCK' => 0x1F553, // Smiley 792 + 'CLOCK_FACE_FOUR_THIRTY' => 0x1F55F, // Smiley 793 + 'CLOCK_FACE_FIVE_OCLOCK' => 0x1F554, // Smiley 794 + 'CLOCK_FACE_FIVE_THIRTY' => 0x1F560, // Smiley 795 + 'CLOCK_FACE_SIX_OCLOCK' => 0x1F555, // Smiley 796 + 'CLOCK_FACE_SEVEN_OCLOCK' => 0x1F556, // Smiley 797 + 'CLOCK_FACE_EIGHT_OCLOCK' => 0x1F557, // Smiley 798 + 'CLOCK_FACE_NINE_OCLOCK' => 0x1F558, // Smiley 799 + 'CLOCK_FACE_TEN_OCLOCK' => 0x1F559, // Smiley 800 + 'CLOCK_FACE_ELEVEN_OCLOCK' => 0x1F55A, // Smiley 801 + 'CLOCK_FACE_SIX_THIRTY' => 0x1F561, // Smiley 802 + 'CLOCK_FACE_SEVEN_THIRTY' => 0x1F562, // Smiley 803 + 'CLOCK_FACE_EIGHT_THIRTY' => 0x1F563, // Smiley 804 + 'CLOCK_FACE_NINE_THIRTY' => 0x1F564, // Smiley 805 + 'CLOCK_FACE_TEN_THIRTY' => 0x1F565, // Smiley 806 + 'CLOCK_FACE_ELEVEN_THIRTY' => 0x1F566, // Smiley 807 + 'HEAVY_MULTIPLICATION_SIGN' => 0x2716, // Smiley 808 + 'HEAVY_PLUS_SIGN' => 0x2795, // Smiley 809 + 'HEAVY_MINUS_SIGN' => 0x2796, // Smiley 810 + 'HEAVY_DIVISION_SIGN' => 0x2797, // Smiley 811 + 'BLACK_SPADE_SUIT' => 0x2660, // Smiley 812 + 'BLACK_HEART_SUIT' => 0x2665, // Smiley 813 + 'BLACK_CLUB_SUIT' => 0x2663, // Smiley 814 + 'BLACK_DIAMOND_SUITE' => 0x2666, // Smiley 815 + 'WHITE_FLOWER' => 0x1F4AE, // Smiley 816 + 'HUNDRED_POINTS_SYMBOL' => 0x1F4AF, // Smiley 817 + 'HEAVY_CHECK_MARK' => 0x2714, // Smiley 818 + 'BALLOT_BOX_WITH_CHECK' => 0x2611, // Smiley 819 + 'RADIO_BUTTON' => 0x1F518, // Smiley 820 + 'LINK_SYMBOL' => 0x1F517, // Smiley 821 + 'CURLY_LOOP' => 0x27B0, // Smiley 822 + 'WAVY_DASH' => 0x3030, // Smiley 823 + 'PART_ALTERNATION_MARK' => 0x303D, // Smiley 824 + 'TRIDENT_EMBLEM' => 0x1F531, // Smiley 825 + 'BLACK_MEDIUM_SQUARE' => 0x25FC, // Smiley 826 + 'WHITE_MEDIUM_SQUARE' => 0x25FB, // Smiley 827 + 'BLACK_MEDIUM_SMALL_SQUARE' => 0x25FE, // Smiley 828 + 'WHITE_MEDIUM_SMALL_SQUARE' => 0x25FD, // Smiley 829 + 'BLACK_SMALL_SQUARE' => 0x25AA, // Smiley 830 + 'WHILTE_SMALL_SQUARE' => 0x25AB, // Smiley 831 + 'UP_POINTING_RED_TRIANGLE' => 0x1F53A, // Smiley 832 + 'BLACK_SQUARE_BUTTON' => 0x1F532, // Smiley 833 + 'WHILTE_SQUARE_BUTTON' => 0x1F533, // Smiley 834 + 'MEDIUM_BLACK_CIRCLE' => 0x26AB, // Smiley 835 + 'MEDIUM_WHITE_CIRCLE' => 0x26AA, // Smiley 836 + 'LARGE_RED_CIRCLE' => 0x1F534, // Smiley 837 + 'LARGE_BLUE_CIRCLE' => 0x1F535, // Smiley 838 + 'DOWN_POINTING_RED_TRIANGLE' => 0x1F53B, // Smiley 839 + 'WHITE_LARGE_SQUARE' => 0x2B1C, // Smiley 840 + 'BLACK_LARGE_SQUARE' => 0x2B1B, // Smiley 841 + 'LARGE_ORANGE_DIAMOND' => 0x1F536, // Smiley 842 + 'LARGE_BLUE_DIAMOND' => 0x1F537, // Smiley 843 + 'SMALL_ORANGE_DIAMOND' => 0x1F538, // Smiley 844 + 'SMALL_BLUE_DIAMOND' => 0x1F539, // Smiley 845 + 'MIDDLEFINGER' => 0x1F595, // Smiley xxx + 'VULCAN_SALUTE' => 0x1F596, // Smiley xxx + ]; + private $listKeys = []; + + public function __construct() + { + $this->listKeys = array_keys($this->listSmileys); + } + + public function __get($name) + { + if (mb_strlen($name) > 7 && substr($name, 0, 7) === 'SMILEY_' && is_numeric(substr($name, 7))) { + $id = (int) substr($name, 7) - 1; + echo 'id '.$id; + if ($id >= 0 && $id < count($this->listSmileys)) { + return $this->__get($this->listKeys[$id]); + } + throw new SmileyNotFoundException(); + } + + if (!isset($this->listSmileys[$name])) { + throw new SmileyNotFoundException(); + } + + if (is_array($this->listSmileys[$name])) { + return implode(array_map([$this, 'ord_unicode'], $this->listSmileys[$name])); + } + + return $this->ord_unicode($this->listSmileys[$name]); + } + + // Source: http://stackoverflow.com/a/7153133/4990350 + + private function ord_unicode($num) + { + if ($num <= 0x7F) { + return chr($num); + } + if ($num <= 0x7FF) { + return chr(($num >> 6) + 192).chr(($num & 63) + 128); + } + if ($num <= 0xFFFF) { + return chr(($num >> 12) + 224).chr((($num >> 6) & 63) + 128).chr(($num & 63) + 128); + } + if ($num <= 0x1FFFFF) { + return chr(($num >> 18) + 240).chr((($num >> 12) & 63) + 128).chr((($num >> 6) & 63) + 128).chr(($num & 63) + 128); + } + + return ''; + } +} diff --git a/src/token.php b/src/token.php index 1d5caf7f..17f1af66 100755 --- a/src/token.php +++ b/src/token.php @@ -1,21 +1,33 @@ = 236 && $token < (236 + count(self::$secondaryStrings))) - { + if (!$subdict && $token >= 236 && $token < (236 + count(self::$secondaryStrings))) { $subdict = true; } - $tokenMap = array(); - if($subdict) - { + + if ($subdict) { $tokenMap = self::$secondaryStrings; - } - else - { + } else { $tokenMap = self::$primaryStrings; } - if($token < 0 || $token > count($tokenMap)) - { - return;//fail + if ($token < 0 || $token > count($tokenMap)) { + return; //fail } + $string = $tokenMap[$token]; - if(!$string) - { - throw new Exception("Invalid token/length in GetToken"); + if (!$string) { + throw new Exception('Invalid token/length in GetToken'); } } -} \ No newline at end of file +} diff --git a/src/vCard.php b/src/vCard.php index 81c70976..65e3ee0c 100755 --- a/src/vCard.php +++ b/src/vCard.php @@ -1,12 +1,11 @@ data = array( - 'display_name' => null, - 'first_name' => null, - 'last_name' => null, - 'additional_name' => null, - 'name_prefix' => null, - 'name_suffix' => null, - 'nickname' => null, - 'title' => null, - 'role' => null, - 'department' => null, - 'company' => null, - 'work_po_box' => null, + $this->data = [ + 'display_name' => null, + 'first_name' => null, + 'last_name' => null, + 'additional_name' => null, + 'name_prefix' => null, + 'name_suffix' => null, + 'nickname' => null, + 'title' => null, + 'role' => null, + 'department' => null, + 'company' => null, + 'work_po_box' => null, 'work_extended_address' => null, - 'work_address' => null, - 'work_city' => null, - 'work_state' => null, - 'work_postal_code' => null, - 'work_country' => null, - 'home_po_box' => null, + 'work_address' => null, + 'work_city' => null, + 'work_state' => null, + 'work_postal_code' => null, + 'work_country' => null, + 'home_po_box' => null, 'home_extended_address' => null, - 'home_address' => null, - 'home_city' => null, - 'home_state' => null, - 'home_postal_code' => null, - 'home_country' => null, - 'office_tel' => null, - 'home_tel' => null, - 'cell_tel' => null, - 'fax_tel' => null, - 'pager_tel' => null, - 'email1' => null, - 'email2' => null, - 'url' => null, - 'photo' => null, - 'birthday' => null, - 'timezone' => null, - 'sort_string' => null, - 'note' => null, - ); + 'home_address' => null, + 'home_city' => null, + 'home_state' => null, + 'home_postal_code' => null, + 'home_country' => null, + 'office_tel' => null, + 'home_tel' => null, + 'cell_tel' => null, + 'fax_tel' => null, + 'pager_tel' => null, + 'email1' => null, + 'email2' => null, + 'url' => null, + 'photo' => null, + 'birthday' => null, + 'timezone' => null, + 'sort_string' => null, + 'note' => null, + ]; return true; } @@ -70,23 +69,25 @@ public function __construct() * Global setter. * * @param string $key - * Name of the property. - * @param mixed $value - * Value to set. + * Name of the property. + * @param mixed $value + * Value to set. * * @return vCard - * Return itself. + * Return itself. */ public function set($key, $value) { // Check if the specified property is defined. if (property_exists($this, $key) && $key != 'data') { $this->{$key} = trim($value); + return $this; } elseif (property_exists($this, $key) && $key == 'data') { foreach ($value as $v_key => $v_value) { $this->{$key}[$v_key] = trim($v_value); } + return $this; } else { return false; @@ -97,13 +98,13 @@ public function set($key, $value) * Checks all the values, builds appropriate defaults for * missing values and generates the vcard data string. */ - function build() + public function build() { if (!$this->class) { $this->class = 'PUBLIC'; } if (!$this->data['display_name']) { - $this->data['display_name'] = $this->data['first_name'] . ' ' . $this->data['last_name']; + $this->data['display_name'] = $this->data['first_name'].' '.$this->data['last_name']; } if (!$this->data['sort_string']) { $this->data['sort_string'] = $this->data['last_name']; @@ -112,7 +113,7 @@ function build() $this->data['sort_string'] = $this->data['company']; } if (!$this->data['timezone']) { - $this->data['timezone'] = date("O"); + $this->data['timezone'] = date('O'); } if (!$this->revisionDate) { $this->revisionDate = date('Y-m-d H:i:s'); @@ -120,118 +121,117 @@ function build() $this->card = "BEGIN:VCARD\r\n"; $this->card .= "VERSION:3.0\r\n"; - $this->card .= "CLASS:" . $this->class . "\r\n"; + $this->card .= 'CLASS:'.$this->class."\r\n"; $this->card .= "PRODID:-//class_vCard from WhatsAPI//NONSGML Version 1//EN\r\n"; - $this->card .= "REV:" . $this->revisionDate . "\r\n"; - $this->card .= "FN:" . $this->data['display_name'] . "\r\n"; - $this->card .= "N:" - . $this->data['last_name'] . ";" - . $this->data['first_name'] . ";" - . $this->data['additional_name'] . ";" - . $this->data['name_prefix'] . ";" - . $this->data['name_suffix'] . "\r\n"; + $this->card .= 'REV:'.$this->revisionDate."\r\n"; + $this->card .= 'FN:'.$this->data['display_name']."\r\n"; + $this->card .= 'N:' + .$this->data['last_name'].';' + .$this->data['first_name'].';' + .$this->data['additional_name'].';' + .$this->data['name_prefix'].';' + .$this->data['name_suffix']."\r\n"; if ($this->data['nickname']) { - $this->card .= "NICKNAME:" . $this->data['nickname'] . "\r\n"; + $this->card .= 'NICKNAME:'.$this->data['nickname']."\r\n"; } if ($this->data['title']) { - $this->card .= "TITLE:" . $this->data['title'] . "\r\n"; + $this->card .= 'TITLE:'.$this->data['title']."\r\n"; } if ($this->data['company']) { - $this->card .= "ORG:" . $this->data['company']; + $this->card .= 'ORG:'.$this->data['company']; } if ($this->data['department']) { - $this->card .= ";" . $this->data['department']; + $this->card .= ';'.$this->data['department']; } $this->card .= "\r\n"; if ($this->data['work_po_box'] || $this->data['work_extended_address'] || $this->data['work_address'] || $this->data['work_city'] || $this->data['work_state'] || $this->data['work_postal_code'] || $this->data['work_country']) { - $this->card .= "ADR;type=WORK:" - . $this->data['work_po_box'] . ";" - . $this->data['work_extended_address'] . ";" - . $this->data['work_address'] . ";" - . $this->data['work_city'] . ";" - . $this->data['work_state'] . ";" - . $this->data['work_postal_code'] . ";" - . $this->data['work_country'] . "\r\n"; + $this->card .= 'ADR;type=WORK:' + .$this->data['work_po_box'].';' + .$this->data['work_extended_address'].';' + .$this->data['work_address'].';' + .$this->data['work_city'].';' + .$this->data['work_state'].';' + .$this->data['work_postal_code'].';' + .$this->data['work_country']."\r\n"; } if ($this->data['home_po_box'] || $this->data['home_extended_address'] || $this->data['home_address'] || $this->data['home_city'] || $this->data['home_state'] || $this->data['home_postal_code'] || $this->data['home_country']) { - $this->card .= "ADR;type=HOME:" - . $this->data['home_po_box'] . ";" - . $this->data['home_extended_address'] . ";" - . $this->data['home_address'] . ";" - . $this->data['home_city'] . ";" - . $this->data['home_state'] . ";" - . $this->data['home_postal_code'] . ";" - . $this->data['home_country'] . "\r\n"; + $this->card .= 'ADR;type=HOME:' + .$this->data['home_po_box'].';' + .$this->data['home_extended_address'].';' + .$this->data['home_address'].';' + .$this->data['home_city'].';' + .$this->data['home_state'].';' + .$this->data['home_postal_code'].';' + .$this->data['home_country']."\r\n"; } if ($this->data['email1']) { - $this->card .= "EMAIL;type=INTERNET,pref:" . $this->data['email1'] . "\r\n"; + $this->card .= 'EMAIL;type=INTERNET,pref:'.$this->data['email1']."\r\n"; } if ($this->data['email2']) { - $this->card .= "EMAIL;type=INTERNET:" . $this->data['email2'] . "\r\n"; + $this->card .= 'EMAIL;type=INTERNET:'.$this->data['email2']."\r\n"; } if ($this->data['office_tel']) { - $this->card .= "TEL;type=WORK,voice:" . $this->data['office_tel'] . "\r\n"; + $this->card .= 'TEL;type=WORK,voice:'.$this->data['office_tel']."\r\n"; } if ($this->data['home_tel']) { - $this->card .= "TEL;type=HOME,voice:" . $this->data['home_tel'] . "\r\n"; + $this->card .= 'TEL;type=HOME,voice:'.$this->data['home_tel']."\r\n"; } if ($this->data['cell_tel']) { - $this->card .= "TEL;type=CELL,voice:" . $this->data['cell_tel'] . "\r\n"; + $this->card .= 'TEL;type=CELL,voice:'.$this->data['cell_tel']."\r\n"; } if ($this->data['fax_tel']) { - $this->card .= "TEL;type=WORK,fax:" . $this->data['fax_tel'] . "\r\n"; + $this->card .= 'TEL;type=WORK,fax:'.$this->data['fax_tel']."\r\n"; } if ($this->data['pager_tel']) { - $this->card .= "TEL;type=WORK,pager:" . $this->data['pager_tel'] . "\r\n"; + $this->card .= 'TEL;type=WORK,pager:'.$this->data['pager_tel']."\r\n"; } if ($this->data['url']) { - $this->card .= "URL;type=WORK:" . $this->data['url'] . "\r\n"; + $this->card .= 'URL;type=WORK:'.$this->data['url']."\r\n"; } if ($this->data['birthday']) { - $this->card .= "BDAY:" . $this->data['birthday'] . "\r\n"; + $this->card .= 'BDAY:'.$this->data['birthday']."\r\n"; } if ($this->data['role']) { - $this->card .= "ROLE:" . $this->data['role'] . "\r\n"; + $this->card .= 'ROLE:'.$this->data['role']."\r\n"; } if ($this->data['note']) { - $this->card .= "NOTE:" . $this->data['note'] . "\r\n"; + $this->card .= 'NOTE:'.$this->data['note']."\r\n"; } - if($this->data['photo']) - { + if ($this->data['photo']) { $this->card .= $this->generatePhotoData(); } - $this->card .= "TZ:" . $this->data['timezone'] . "\r\n"; + $this->card .= 'TZ:'.$this->data['timezone']."\r\n"; $this->card .= "END:VCARD\r\n"; } protected function generatePhotoData() { $photo = $this->data['photo']; - $data = "PHOTO;"; + $data = 'PHOTO;'; //detect type - if(substr($photo, 0, 4) == 'http') - { + if (substr($photo, 0, 4) == 'http') { //url - $data .= 'URL:' . $photo; - } - else - { + $data .= 'URL:'.$photo; + } else { //path $bindata = file_get_contents($photo); $bindata = base64_encode($bindata); - $data .= 'BASE64:' . $bindata; + $data .= 'BASE64:'.$bindata; } $data .= "\r\n"; + return $data; } /** * Streams the vcard to the browser client. + * + * @return bool */ - function download() + public function download() { if (!$this->card) { $this->build(); @@ -243,18 +243,20 @@ function download() $this->filename = str_replace(' ', '_', $this->filename); - header("Content-type: text/directory"); - header("Content-Disposition: attachment; filename=" . $this->filename . ".vcf"); - header("Pragma: public"); + header('Content-type: text/directory'); + header('Content-Disposition: attachment; filename='.$this->filename.'.vcf'); + header('Pragma: public'); echo $this->card; return true; } /** - * Show the vcard. + * Show the vCard. + * + * @return object vCard */ - function show() + public function show() { if (!$this->card) { $this->build(); @@ -262,5 +264,4 @@ function show() return $this->card; } - } diff --git a/src/wadata/logs/.gitkeep b/src/wadata/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/wadata/media/.gitkeep b/src/wadata/media/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/wadata/pictures/.gitkeep b/src/wadata/pictures/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/whatsprot.class.php b/src/whatsprot.class.php old mode 100755 new mode 100644 index f11fc9ec..e77237c8 --- a/src/whatsprot.class.php +++ b/src/whatsprot.class.php @@ -1,125 +1,157 @@ index = $index; - $this->syncId = $syncId; - $this->existing = $existing; - $this->nonExisting = $nonExisting; - } +require_once 'events/WhatsApiEventsManager.php'; +require_once 'SqliteMessageStore.php'; +require_once 'SqliteAxolotlStore.php'; +require_once 'handlers/NotificationHandler.php'; +require_once 'handlers/MessageHandler.php'; +require_once 'handlers/IqHandler.php'; +if (extension_loaded('curve25519') && extension_loaded('protobuf')) { + require_once 'pb_wa_messages.php'; + require_once 'libaxolotl-php/util/KeyHelper.php'; + require_once 'libaxolotl-php/ecc/Curve.php'; + require_once 'libaxolotl-php/state/PreKeyRecord.php'; + require_once 'libaxolotl-php/state/PreKeyBundle.php'; + require_once 'libaxolotl-php/SessionBuilder.php'; + require_once 'libaxolotl-php/SessionCipher.php'; + require_once 'libaxolotl-php/groups/GroupCipher.php'; + require_once 'libaxolotl-php/groups/GroupSessionBuilder.php'; } class WhatsProt { - /** - * Constant declarations. - */ - const CONNECTED_STATUS = 'connected'; // Describes the connection status with the WhatsApp server. - const DISCONNECTED_STATUS = 'disconnected'; // Describes the connection status with the WhatsApp server. - const MEDIA_FOLDER = 'media'; // The relative folder to store received media files - const PICTURES_FOLDER = 'pictures'; // The relative folder to store picture files - const PORT = 443; // The port of the WhatsApp server. - const TIMEOUT_SEC = 2; // The timeout for the connection with the WhatsApp servers. - const TIMEOUT_USEC = 0; // - const WHATSAPP_CHECK_HOST = 'v.whatsapp.net/v2/exist'; // The check credentials host. - const WHATSAPP_GROUP_SERVER = 'g.us'; // The Group server hostname - const WHATSAPP_HOST = 'c.whatsapp.net'; // The hostname of the WhatsApp server. - const WHATSAPP_REGISTER_HOST = 'v.whatsapp.net/v2/register'; // The register code host. - const WHATSAPP_REQUEST_HOST = 'v.whatsapp.net/v2/code'; // The request code host. - const WHATSAPP_SERVER = 's.whatsapp.net'; // The hostname used to login/send messages. - const WHATSAPP_UPLOAD_HOST = 'https://mms.whatsapp.net/client/iphone/upload.php'; // The upload host. - const WHATSAPP_DEVICE = 'Android'; // The device name. - const WHATSAPP_VER = '2.11.407'; // The WhatsApp version. - const WHATSAPP_USER_AGENT = 'WhatsApp/2.11.407 Android/4.3 Device/GalaxyS3'; // User agent used in request/registration code. - const WHATSAPP_VER_CHECKER = 'http://www.whatsapp.com/android/current/WhatsApp.version'; - /** * Property declarations. */ protected $accountInfo; // The AccountInfo object. - protected $challengeFilename = 'nextChallenge.dat'; + protected $challengeFilename; // Path to nextChallenge.dat. protected $challengeData; // protected $debug; // Determines whether debug mode is on or off. - protected $event; // An instance of the WhatsAppEvent class. - protected $groupList = array(); // An array with all the groups a user belongs in. - protected $identity; // The Device Identity token. Obtained during registration with this API or using Missvenom to sniff from your phone. - protected $inputKey; // Instances of the KeyStream class. + protected $eventManager; // An instance of the WhatsApiEvent Manager. + protected $groupList = []; // An array with all the groups a user belongs in. protected $outputKey; // Instances of the KeyStream class. protected $groupId = false; // Id of the group created. protected $lastId = false; // Id to the last message sent. protected $loginStatus; // Holds the login status. - protected $mediaFileInfo = array(); // Media File Information - protected $mediaQueue = array(); // Queue for media message nodes - protected $messageCounter = 1; // Message counter for auto-id. - protected $messageQueue = array(); // Queue for received messages. + protected $mediaFileInfo = []; // Media File Information + protected $mediaQueue = []; // Queue for media message nodes + protected $messageCounter = 0; // Message counter for auto-id. + protected $iqCounter = 1; + protected $messageQueue = []; // Queue for received messages. protected $name; // The user name. protected $newMsgBind = false; // - protected $outQueue = array(); // Queue for outgoing messages. + protected $outQueue = []; // Queue for outgoing messages. protected $password; // The user password. protected $phoneNumber; // The user phone number including the country code without '+' or '00'. - public $reader; // An instance of the BinaryTreeNodeReader class. protected $serverReceivedId; // Confirm that the *server* has received your command. protected $socket; // A socket to connect to the WhatsApp network. - protected $writer; // An instance of the BinaryTreeNodeWriter class. + protected $messageStore; + protected $nodeId = []; + protected $messageId; + protected $voice; + protected $timeout = 0; + protected $sessionCiphers = []; + public $v2Jids = []; + public $v1Only = []; + protected $groupCiphers = []; + protected $pending_nodes = []; + protected $replaceKey; + public $retryCounters = []; + protected $readReceipts = true; + public $retryNodes = []; + protected $axolotlStore; + protected $pingCounter = 1; + public $writer; // An instance of the BinaryTreeNodeWriter class. + public $reader; // An instance of the BinaryTreeNodeReader class. + public $logger; + public $log; + public $dataFolder; // /** * Default class constructor. * * @param string $number - * The user phone number including the country code without '+' or '00'. - * @param string $identity - * The Device Identity token. Obtained during registration with this API - * or using Missvenom to sniff from your phone. + * The user phone number including the country code without '+' or '00'. * @param string $nickname - * The user name. + * The user name. * @param $debug * Debug on or off, false by default. + * @param $log + * Enable log, false by default. + * @param $datafolder + * The folder for whatsapp data like MEDIA, PICTURES etc.. By default that is wadata in src folder */ - public function __construct($number, $identity, $nickname, $debug = false) + public function __construct($number, $nickname, $debug = false, $log = false, $datafolder = null) { $this->writer = new BinTreeNodeWriter(); $this->reader = new BinTreeNodeReader(); $this->debug = $debug; $this->phoneNumber = $number; - if (!$this->checkIdentity($identity)) { - //compute identity with pseudo_random_bytes - $this->identity = $this->buildIdentity($identity); + + if ($datafolder !== null && file_exists($datafolder)) { + if (substr(trim($datafolder), -1) == DIRECTORY_SEPARATOR) { + $this->dataFolder = $datafolder; + } else { + $this->dataFolder = $datafolder.DIRECTORY_SEPARATOR; + } } else { - //use provided identity hash - $this->identity = file_get_contents($identity.'.dat'); + $this->dataFolder = __DIR__.DIRECTORY_SEPARATOR.Constants::DATA_FOLDER.DIRECTORY_SEPARATOR; + } + + if (!file_exists($this->dataFolder.Constants::MEDIA_FOLDER)) { + mkdir($this->dataFolder.Constants::MEDIA_FOLDER, 0777, true); } + + if (!file_exists($this->dataFolder.Constants::PICTURES_FOLDER)) { + mkdir($this->dataFolder.Constants::PICTURES_FOLDER, 0777, true); + } + + if (!file_exists($this->dataFolder.'logs')) { + mkdir($this->dataFolder.'logs', 0777, true); + } + + //wadata/nextChallenge.12125557788.dat + $this->challengeFilename = sprintf('%snextChallenge.%s.dat', $this->dataFolder, $number); + $this->setMessageStore(new SqliteMessageStore($number, $this->dataFolder)); + $this->log = $log; + if ($log) { + $this->logger = new Logger($this->dataFolder. + 'logs'.DIRECTORY_SEPARATOR.$number.'.log'); + } + + $this->setAxolotlStore(new axolotlSqliteStore($number, $this->dataFolder)); + $this->name = $nickname; - $this->loginStatus = static::DISCONNECTED_STATUS; + $this->loginStatus = Constants::DISCONNECTED_STATUS; + $this->eventManager = new WhatsApiEventsManager(); } /** - * If you need use diferent challenge fileName you can use this + * If you need use different challenge fileName you can use this. * * @param string $filename */ - public function setChallengeName($filename){ + public function setChallengeName($filename) + { $this->challengeFilename = $filename; } /** * Add message to the outgoing queue. + * + * @param $node */ public function addMsgOutQueue($node) { @@ -127,1050 +159,1330 @@ public function addMsgOutQueue($node) } /** - * Check if account credentials are valid. - * - * WARNING: WhatsApp now changes your password everytime you use this. - * Make sure you update your config file if the output informs about - * a password change. - * - * @return object - * An object with server response. - * - status: Account status. - * - login: Phone number with country code. - * - pw: Account password. - * - type: Type of account. - * - expiration: Expiration date in UNIX TimeStamp. - * - kind: Kind of account. - * - price: Formatted price of account. - * - cost: Decimal amount of account. - * - currency: Currency price of account. - * - price_expiration: Price expiration in UNIX TimeStamp. + * Connect (create a socket) to the WhatsApp network. * - * @throws Exception + * @return bool */ - public function checkCredentials() + public function connect() { - if (!$phone = $this->dissectPhone()) { - throw new Exception('The provided phone number is not valid.'); + if ($this->isConnected()) { + return true; } - if ($countryCode == null && $phone['ISO3166'] != '') { - $countryCode = $phone['ISO3166']; - } - if ($countryCode == null) { - $countryCode = 'US'; - } - if ($langCode == null && $phone['ISO639'] != '') { - $langCode = $phone['ISO639']; - } - if ($langCode == null) { - $langCode = 'en'; + /* Create a TCP/IP socket. */ + $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($socket !== false) { + $result = socket_connect($socket, 'e'.rand(1, 16).'.whatsapp.net', Constants::PORT); + if ($result === false) { + $socket = false; + } } - // Build the url. - $host = 'https://' . static::WHATSAPP_CHECK_HOST; - $query = array( - 'cc' => $phone['cc'], - 'in' => $phone['phone'], - 'id' => $this->identity, - 'lg' => $langCode, - 'lc' => $countryCode, - ); + if ($socket !== false) { + socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => Constants::TIMEOUT_SEC, 'usec' => Constants::TIMEOUT_USEC]); + socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => Constants::TIMEOUT_SEC, 'usec' => Constants::TIMEOUT_USEC]); - $response = $this->getResponse($host, $query); + $this->socket = $socket; + $this->eventManager()->fire('onConnect', + [ + $this->phoneNumber, + $this->socket, + ] + ); + $this->logFile('info', 'Connected to WA server'); - if ($response->status != 'ok') { - $this->eventManager()->fireCredentialsBad($this->phoneNumber, $response->status, $response->reason); - if ($this->debug) { - print_r($query); - print_r($response); - } - throw new Exception('There was a problem trying to request the code.'); + return true; } else { - $this->eventManager()->fireCredentialsGood( - $this->phoneNumber, - $response->login, - $response->pw, - $response->type, - $response->expiration, - $response->kind, - $response->price, - $response->cost, - $response->currency, - $response->price_expiration + $this->logFile('error', 'Failed to connect WA server'); + $this->eventManager()->fire('onConnectError', + [ + $this->phoneNumber, + $this->socket, + ] ); - } - return $response; + return false; + } } /** - * Register account on WhatsApp using the provided code. - * - * @param integer $code - * Numeric code value provided on requestCode(). - * - * @return object - * An object with server response. - * - status: Account status. - * - login: Phone number with country code. - * - pw: Account password. - * - type: Type of account. - * - expiration: Expiration date in UNIX TimeStamp. - * - kind: Kind of account. - * - price: Formatted price of account. - * - cost: Decimal amount of account. - * - currency: Currency price of account. - * - price_expiration: Price expiration in UNIX TimeStamp. + * Do we have an active socket connection to WhatsApp? * - * @throws Exception + * @return bool */ - public function codeRegister($code) + public function isConnected() { - if (!$phone = $this->dissectPhone()) { - throw new Exception('The provided phone number is not valid.'); - } - - if ($countryCode == null && $phone['ISO3166'] != '') { - $countryCode = $phone['ISO3166']; - } - if ($countryCode == null) { - $countryCode = 'US'; - } - if ($langCode == null && $phone['ISO639'] != '') { - $langCode = $phone['ISO639']; - } - if ($langCode == null) { - $langCode = 'en'; - } + return $this->socket !== null; + } - // Build the url. - $host = 'https://' . static::WHATSAPP_REGISTER_HOST; - $query = array( - 'cc' => $phone['cc'], - 'in' => $phone['phone'], - 'id' => $this->identity, - 'code' => $code, - 'lg' => $langCode, - 'lc' => $countryCode, - ); + /** + * Disconnect from the WhatsApp network. + */ + public function disconnect() + { + if (is_resource($this->socket)) { + @socket_shutdown($this->socket, 2); + @socket_close($this->socket); + } + $this->socket = null; + $this->loginStatus = Constants::DISCONNECTED_STATUS; + $this->logFile('info', 'Disconnected from WA server'); + $this->eventManager()->fire('onDisconnect', + [ + $this->phoneNumber, + $this->socket, + ] + ); + } - $response = $this->getResponse($host, $query); + /** + * @return WhatsApiEventsManager + */ + public function eventManager() + { + return $this->eventManager; + } + /** + * Enable / Disable automatic read receipt + * This is enabled by default. + */ + public function enableReadReceipt($enable) + { + $this->readReceipts = $enable; + } - if ($response->status != 'ok') { - $this->eventManager()->fireCodeRegisterFailed( - $this->phoneNumber, - $response->status, - $response->reason, - $response->retry_after - ); - if ($this->debug) { - print_r($query); - print_r($response); - } - throw new Exception('An error occurred registering the registration code from WhatsApp.'); - } else { - $this->eventManager()->fireCodeRegister( - $this->phoneNumber, - $response->login, - $response->pw, - $response->type, - $response->expiration, - $response->kind, - $response->price, - $response->cost, - $response->currency, - $response->price_expiration - ); - } + /** + * Drain the message queue for application processing. + * + * @return ProtocolNode[] + * Return the message queue list. + */ + public function getMessages() + { + $ret = $this->messageQueue; + $this->messageQueue = []; - return $response; + return $ret; } /** - * Request a registration code from WhatsApp. + * Login to the WhatsApp server with your password. * - * @param string $method - * Accepts only 'sms' or 'voice' as a value. - * @param string $countryCode - * ISO Country Code, 2 Digit. - * @param string $langCode - * ISO 639-1 Language Code: two-letter codes. - * - * @return object - * An object with server response. - * - status: Status of the request (sent/fail). - * - length: Registration code lenght. - * - method: Used method. - * - reason: Reason of the status (e.g. too_recent/missing_param/bad_param). - * - param: The missing_param/bad_param. - * - retry_after: Waiting time before requesting a new code. + * If you already know your password you can log into the Whatsapp server + * using this method. * - * @throws Exception + * @param string $password Your whatsapp password. You must already know this! */ - public function codeRequest($method = 'sms', $countryCode = null, $langCode = null) + public function loginWithPassword($password) { - if (!$phone = $this->dissectPhone()) { - throw new Exception('The provided phone number is not valid.'); + $this->password = $password; + if (is_readable($this->challengeFilename)) { + $challengeData = file_get_contents($this->challengeFilename); + if ($challengeData) { + $this->challengeData = $challengeData; + } } + $login = new Login($this, $this->password); + $login->doLogin(); + } - if ($countryCode == null && $phone['ISO3166'] != '') { - $countryCode = $phone['ISO3166']; - } - if ($countryCode == null) { - $countryCode = 'US'; - } - if ($langCode == null && $phone['ISO639'] != '') { - $langCode = $phone['ISO639']; - } - if ($langCode == null) { - $langCode = 'en'; + /** + * Fetch a single message node. + * + * @throws Exception + * + * @return bool + */ + public function pollMessage() + { + if (!$this->isConnected()) { + throw new ConnectionException('Connection Closed!'); } - // Build the token. - $token = generateRequestToken($phone['country'], $phone['phone']); + $r = [$this->socket]; + $w = []; + $e = []; + $s = socket_select($r, $w, $e, Constants::TIMEOUT_SEC, Constants::TIMEOUT_USEC); - // Build the url. - $host = 'https://' . static::WHATSAPP_REQUEST_HOST; - $query = array( - 'method' => $method, - 'in' => $phone['phone'], - 'cc' => $phone['cc'], - 'id' => $this->identity, - 'lg' => $langCode, - 'lc' => $countryCode, - 'token' => urlencode($token), - 'sim_mcc' => '000', //$phone['mcc'] - 'sim_mnc' => '000', // 001 - //'reason' => 'jailbroken', - ); + if ($s) { - if ($this->debug) { - print_r($query); - } + // Something to read + if ($stanza = $this->readStanza()) { + $this->processInboundData($stanza); - $response = $this->getResponse($host, $query); - - if ($this->debug) { - print_r($response); + return true; + } } - - if ($response->status == 'ok') { - $this->eventManager()->fireCodeRegister( - $this->phoneNumber, - $response->login, - $response->pw, - $response->type, - $response->expiration, - $response->kind, - $response->price, - $response->cost, - $response->currency, - $response->price_expiration - ); - } else if ($response->status != 'sent') { - if (isset($response->reason) && $response->reason == "too_recent") { - $this->eventManager()->fireCodeRequestFailedTooRecent( - $this->phoneNumber, - $method, - $response->reason, - $response->retry_after - ); - $minutes = round($response->retry_after / 60); - throw new Exception("Code already sent. Retry after $minutes minutes."); - } else { - $this->eventManager()->fireCodeRequestFailed( - $this->phoneNumber, - $method, - $response->reason, - $response->param - ); - throw new Exception('There was a problem trying to request the code.'); + if (time() - $this->timeout > 60) { + if ($this->pingCounter >= 3) + { + $this->sendOfflineStatus(); + $this->disconnect(); + $this->iqCounter = 1; + $this->connect(); + $this->loginWithPassword($this->password); + $this->pingCounter = 1; + } + else { + $this->sendPing(); + $this->pingCounter++; } - } else { - $this->eventManager()->fireCodeRequest( - $this->phoneNumber, - $method, - $response->length - ); } - return $response; + return false; } /** - * Connect (create a socket) to the WhatsApp network. + * Send the active status. User will show up as "Online" (as long as socket is connected). */ - public function connect() + public function sendActiveStatus() { + $messageNode = new ProtocolNode('presence', ['type' => 'active'], null, ''); + $this->sendNode($messageNode); + } - $WAver = trim(file_get_contents(static::WHATSAPP_VER_CHECKER)); - - $WAverS = str_replace(".","",$WAver); - $ver = str_replace(".","",static::WHATSAPP_VER); - - if($ver>=$WAverS) - { - echo "\nUp to date :)\n\n"; - } - else{ - $classesMD5 = file_get_contents('https://mgp25.com/WAToken/WhatsApp.token'); + public function sendSetPreKeys($new = false) + { + $axolotl = new KeyHelper(); - updateData('token.php', $WAver, $classesMD5); - updateData('whatsprot.class.php', $WAver); + $identityKeyPair = $axolotl->generateIdentityKeyPair(); + $privateKey = $identityKeyPair->getPrivateKey()->serialize(); + $publicKey = $identityKeyPair->getPublicKey()->serialize(); + $keys = $axolotl->generatePreKeys(mt_rand(), 200); + $this->axolotlStore->storePreKeys($keys); - echo "\n Token, User Agent and version were updated :)\n"; + for ($i = 0; $i < 200; $i++) { + $prekeyId = adjustId($keys[$i]->getId()); + $prekey = substr($keys[$i]->getKeyPair()->getPublicKey()->serialize(), 1); + $id = new ProtocolNode('id', null, null, $prekeyId); + $value = new ProtocolNode('value', null, null, $prekey); + $prekeys[] = new ProtocolNode('key', null, [$id, $value], null); // 200 PreKeys } - $Socket = fsockopen(static::WHATSAPP_HOST, static::PORT); - if ($Socket !== false) { - stream_set_timeout($Socket, static::TIMEOUT_SEC, static::TIMEOUT_USEC); - $this->socket = $Socket; - $this->eventManager()->fireConnect( - $this->phoneNumber, - $this->socket - ); + if ($new) { + $registrationId = $this->axolotlStore->getLocalRegistrationId(); } else { - if ($this->debug) { - print_r("Firing onConnectError\n"); - } - $this->eventManager()->fireConnectError( - $this->phoneNumber, - $this->socket - ); + $registrationId = $axolotl->generateRegistrationId(); } - } + $registration = new ProtocolNode('registration', null, null, adjustId($registrationId)); + $identity = new ProtocolNode('identity', null, null, substr($publicKey, 1)); + $type = new ProtocolNode('type', null, null, chr(Curve::DJB_TYPE)); - /** - * Disconnect from the WhatsApp network. - */ - public function disconnect() - { - if (is_resource($this->socket)) { - fclose($this->socket); - $this->eventManager()->fireDisconnect( - $this->phoneNumber, - $this->socket - ); - } - } + $this->axolotlStore->storeLocalData($registrationId, $identityKeyPair); - /** - * Gets a new micro event dispatcher. - * - * @return WhatsAppEvent The event manager. - */ - public function eventManager() - { - if (!is_object($this->event)) { - $this->event = new WhatsAppEvent(); - } + $list = new ProtocolNode('list', null, $prekeys, null); - return $this->event; - } + $signedRecord = $axolotl->generateSignedPreKey($identityKeyPair, $axolotl->getRandomSequence(65536)); + $this->axolotlStore->storeSignedPreKey($signedRecord->getId(), $signedRecord); - /** - * Drain the message queue for application processing. - * - * @return ProtocolNode[] - * Return the message queue list. - */ - public function getMessages() - { - $ret = $this->messageQueue; - $this->messageQueue = array(); + $sid = new ProtocolNode('id', null, null, adjustId($signedRecord->getId())); + $value = new ProtocolNode('value', null, null, substr($signedRecord->getKeyPair()->getPublicKey()->serialize(), 1)); + $signature = new ProtocolNode('signature', null, null, $signedRecord->getSignature()); - return $ret; + $secretKey = new ProtocolNode('skey', null, [$sid, $value, $signature], null); + + $iqId = $this->nodeId['sendcipherKeys'] = $this->createIqId(); + $iqNode = new ProtocolNode('iq', + [ + 'id' => $iqId, + 'to' => Constants::WHATSAPP_SERVER, + 'type' => 'set', + 'xmlns' => 'encrypt', + ], [$identity, $registration, $type, $list, $secretKey], null); + $this->sendNode($iqNode); + $this->waitForServer($iqId); } /** - * Log into the Whatsapp server. + * Send a request to get cipher keys from an user. * - * ###Warning### using this method will generate a new password - * from the WhatsApp servers each time. - * - * If you know your password and wish to use it without generating - * a new password - use the loginWithPassword() method instead. + * @param $number + * Phone number of the user you want to get the cipher keys. */ - public function login() + public function sendGetCipherKeysFromUser($numbers, $replaceKey = false) { - $this->accountInfo = (array) $this->checkCredentials(); - if ($this->accountInfo['status'] == 'ok') { - if ($this->debug) { - print_r("New password received: " . $this->accountInfo['pw'] . "\n"); - } - $this->password = $this->accountInfo['pw']; + if (!is_array($numbers)) { + $numbers = [$numbers]; } - $this->doLogin(); - } - /** - * Login to the Whatsapp server with your password - * - * If you already know your password you can log into the Whatsapp server - * using this method. - * - * @param string $password Your whatsapp password. You must already know this! - */ - public function loginWithPassword($password) - { - $this->password = $password; - $challengeData = @file_get_contents($this->challengeFilename); - if($challengeData) { - $this->challengeData = $challengeData; + $this->replaceKey = $replaceKey; + $msgId = $this->nodeId['cipherKeys'] = $this->createIqId(); + + $userNode = []; + foreach ($numbers as $number) { + $userNode[] = new ProtocolNode('user', + [ + 'jid' => $this->getJID($number), + ], null, null); } - $this->doLogin(); + $keyNode = new ProtocolNode('key', null, $userNode, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'encrypt', + 'type' => 'get', + 'to' => Constants::WHATSAPP_SERVER, + ], [$keyNode], null); + + $this->sendNode($node); + $this->waitForServer($msgId); } - /** - * Fetch a single message node - * @param bool $autoReceipt - * @return bool - */ - public function pollMessage($autoReceipt = true) + public function resetEncryption() { - $stanza = $this->readStanza(); - if($stanza) - { - $this->processInboundData($stanza, $autoReceipt); - return true; + if ($this->axolotlStore) { + $this->axolotlStore->clear(); + } + $this->retryCounters = []; + $this->sendSetPreKeys(); + $this->pollMessage(); + $this->pollMessage(); + $this->disconnect(); + $this->connect(); + $this->loginWithPassword($this->password); + foreach ($this->retryNodes as $node) { + $this->processInboundDataNode($node); } - return false; } - /** - * Send the active status. User will show up as "Online" (as long as socket is connected). - */ - public function sendActiveStatus() + public function sendRetry($node, $to, $id, $t, $participant = null) { - $messageNode = new ProtocolNode("presence", array("type" => "active"), null, ""); - $this->sendNode($messageNode); + if (!isset($this->retryCounters[$id])) { + $this->retryCounters[$id] = 1; + } else { + if (!isset($this->retryNodes[$id])) { + $this->retryNodes[$id] = $node; + } elseif ($this->retryCounters[$id] > 2) { + $this->resetEncryption(); + } + } + $retryNode = new ProtocolNode('retry', + [ + 'v' => '1', + 'count' => '1', //$this->retryCounters[$id] + 'id' => $id, + 't' => $t, + ], null, null); + $registrationNode = new ProtocolNode('registration', null, null, adjustId($this->axolotlStore->getLocalRegistrationId())); + if ($participant != null) { //isgroups + //group retry + $node = new ProtocolNode('receipt', + [ + 'id' => $id, + 'to' => $to, + 'participant' => $participant, + 'type' => 'retry', + 't' => $t, + ], [$retryNode, $registrationNode], null); + } else { + $node = new ProtocolNode('receipt', + [ + 'id' => $id, + 'to' => $to, + 'type' => 'retry', + 't' => $t, + ], [$retryNode, $registrationNode], null); + if (!isset($this->retryCounters[$id])) { + $this->retryCounters[$id] = 0; + } + $this->retryCounters[$id]++; + } + $this->sendNode($node); + $this->waitForServer($id); } /** * Send a Broadcast Message with audio. * - * The receiptiant MUST have your number (synced) and in their contact list + * The recipients MUST have your number (synced) and in their contact list * otherwise the message will not deliver to that person. * * Approx 20 (unverified) is the maximum number of targets * - * @param array $targets An array of numbers to send to. - * @param string $path URL or local path to the audio file to send - * @param bool $storeURLmedia Keep a copy of the audio file on your server + * @param array $targets An array of numbers to send to. + * @param string $path URL or local path to the audio file to send + * @param bool $storeURLmedia Keep a copy of the audio file on your server + * @param int $fsize + * @param string $fhash + * + * @return string|null Message ID if successfully, null if not. */ - public function sendBroadcastAudio($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = "") + public function sendBroadcastAudio($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = '') { if (!is_array($targets)) { - $targets = array($targets); + $targets = [$targets]; } - $this->sendMessageAudio($targets, $path, $storeURLmedia, $fsize, $fhash); + // Return message ID. Make pull request for this. + return $this->sendMessageAudio($targets, $path, $storeURLmedia, $fsize, $fhash); } /** * Send a Broadcast Message with an image. * - * The receiptiant MUST have your number (synced) and in their contact list + * The recipients MUST have your number (synced) and in their contact list * otherwise the message will not deliver to that person. * * Approx 20 (unverified) is the maximum number of targets * - * @param array $targets An array of numbers to send to. - * @param string $path URL or local path to the image file to send - * @param bool $storeURLmedia Keep a copy of the audio file on your server + * @param array $targets An array of numbers to send to. + * @param string $path URL or local path to the image file to send + * @param bool $storeURLmedia Keep a copy of the audio file on your server + * @param int $fsize + * @param string $fhash + * @param string $caption + * + * @return string|null Message ID if successfully, null if not. */ - public function sendBroadcastImage($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = "") + public function sendBroadcastImage($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = '', $caption = '') { if (!is_array($targets)) { - $targets = array($targets); + $targets = [$targets]; } - $this->sendMessageImage($targets, $path, $storeURLmedia, $fsize, $fhash); + // Return message ID. Make pull request for this. + return $this->sendMessageImage($targets, $path, $storeURLmedia, $fsize, $fhash, $caption); } /** * Send a Broadcast Message with location data. * - * The receiptiant MUST have your number (synced) and in their contact list + * The recipients MUST have your number (synced) and in their contact list * otherwise the message will not deliver to that person. * * If no name is supplied , receiver will see large sized google map * thumbnail of entered Lat/Long but NO name/url for location. * * With name supplied, a combined map thumbnail/name box is displayed - * Approx 20 (unverified) is the maximum number of targets * - * @param array $targets An array of numbers to send to. - * @param float $long The longitude of the location eg 54.31652 - * @param float $lat The latitude if the location eg -6.833496 - * @param string $name (Optional) A name to describe the location - * @param string $url (Optional) A URL to link location to web resource + * @param array $targets An array of numbers to send to. + * @param float $long The longitude of the location eg 54.31652 + * @param float $lat The latitude if the location eg -6.833496 + * @param string $name (Optional) A name to describe the location + * @param string $url (Optional) A URL to link location to web resource + * + * @return string Message ID */ - - public function sendBroadcastLocation($targets, $long, $lat, $name = null, $url = null) { if (!is_array($targets)) { - $targets = array($targets); + $targets = [$targets]; } - $this->sendMessageLocation($targets, $long, $lat, $name, $url); + // Return message ID. Make pull request for this. + return $this->sendMessageLocation($targets, $long, $lat, $name, $url); } /** - * Send a Broadcast Message + * Send a Broadcast Message. * - * The receiptiant MUST have your number (synced) and in their contact list + * The recipients MUST have your number (synced) and in their contact list * otherwise the message will not deliver to that person. * * Approx 20 (unverified) is the maximum number of targets * - * @param array $targets An array of numbers to send to. - * @param string $message Your message + * @param array $targets An array of numbers to send to. + * @param string $message Your message + * + * @return string Message ID */ public function sendBroadcastMessage($targets, $message) { - $message = $this->parseMessageForEmojis($message); - $bodyNode = new ProtocolNode("body", null, null, $message); - $this->sendBroadcast($targets, $bodyNode, "text"); + $bodyNode = new ProtocolNode('body', null, null, $message); + // Return message ID. Make pull request for this. + return $this->sendBroadcast($targets, $bodyNode, 'text'); } /** * Send a Broadcast Message with a video. * - * The receiptiant MUST have your number (synced) and in their contact list + * The recipients MUST have your number (synced) and in their contact list * otherwise the message will not deliver to that person. * * Approx 20 (unverified) is the maximum number of targets * - * @param array $targets An array of numbers to send to. - * @param string $path URL or local path to the video file to send - * @param bool $storeURLmedia Keep a copy of the audio file on your server + * @param array $targets An array of numbers to send to. + * @param string $path URL or local path to the video file to send + * @param bool $storeURLmedia Keep a copy of the audio file on your server + * @param int $fsize + * @param string $fhash + * @param string $caption + * + * @return string|null Message ID if successfully, null if not. */ - public function sendBroadcastVideo($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = "") + public function sendBroadcastVideo($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = '', $caption = '') { if (!is_array($targets)) { - $targets = array($targets); + $targets = [$targets]; + } + // Return message ID. Make pull request for this. + return $this->sendMessageVideo($targets, $path, $storeURLmedia, $fsize, $fhash, $caption); + } + + /** + * Delete Broadcast lists. + * + * @param string array $lists + * Contains the broadcast-id list + */ + public function sendDeleteBroadcastLists($lists) + { + $msgId = $this->createIqId(); + $listNode = []; + if ($lists != null && count($lists) > 0) { + for ($i = 0; $i < count($lists); $i++) { + $listNode[$i] = new ProtocolNode('list', ['id' => $lists[$i]], null, null); + } + } else { + $listNode = null; } - $this->sendMessageVideo($targets, $path, $storeURLmedia, $fsize, $fhash); + $deleteNode = new ProtocolNode('delete', null, $listNode, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'w:b', + 'type' => 'set', + 'to' => Constants::WHATSAPP_SERVER, + ], [$deleteNode], null); + + $this->sendNode($node); } /** - * Clears the "dirty" status on your account + * Clears the "dirty" status on your account. * - * @param array $categories + * @param array $categories */ - protected function sendClearDirty($categories) + public function sendClearDirty($categories) { - $msgId = $this->createMsgId("cleardirty"); + $msgId = $this->createIqId(); - $catnodes = array(); + $catnodes = []; foreach ($categories as $category) { - $catnode = new ProtocolNode("clean", array("type" => $category), null, null); + $catnode = new ProtocolNode('clean', ['type' => $category], null, null); $catnodes[] = $catnode; } - $node = new ProtocolNode("iq", array( - "id" => $msgId, - "type" => "set", - "to" => "s.whatsapp.net", - "xmlns" => "urn:xmpp:whatsapp:dirty" - ), $catnodes, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'type' => 'set', + 'to' => Constants::WHATSAPP_SERVER, + 'xmlns' => 'urn:xmpp:whatsapp:dirty', + ], $catnodes, null); + $this->sendNode($node); } public function sendClientConfig() { - $phone = $this->dissectPhone(); + $attr = []; + $attr['platform'] = Constants::PLATFORM; + $attr['version'] = Constants::WHATSAPP_VER; + $child = new ProtocolNode('config', $attr, null, ''); + $node = new ProtocolNode('iq', + [ + 'id' => $this->createIqId(), + 'type' => 'set', + 'xmlns' => 'urn:xmpp:whatsapp:push', + 'to' => Constants::WHATSAPP_SERVER, + ], [$child], null); + + $this->sendNode($node); + } + + public function sendSetGCM($gcm = null) + { + if (is_null($gcm)) { + $gcm = getRandomGCM(); + } + $attr = []; + $attr['platform'] = 'gcm'; + $attr['id'] = $gcm; + $child = new ProtocolNode('config', $attr, null, ''); + $node = new ProtocolNode('iq', + [ + 'id' => $this->createIqId(), + 'type' => 'set', + 'xmlns' => 'urn:xmpp:whatsapp:push', + 'to' => Constants::WHATSAPP_SERVER, + ], [$child], null); - $attr = array(); - $attr["platform"] = "none"; - $attr["lc"] = $phone["ISO3166"]; - $attr["lg"] = $phone["ISO639"]; - $child = new ProtocolNode("config", $attr, null, ""); - $node = new ProtocolNode("iq", array("id" => $this->createMsgId("config"), "type" => "set", "xmlns" => "urn:xmpp:whatsapp:push", "to" => static::WHATSAPP_SERVER), array($child), null); $this->sendNode($node); } public function sendGetClientConfig() { - $msgId = $this->createMsgId("sendconfig"); - $child = new ProtocolNode("config", array("xmlns" => "urn:xmpp:whatsapp:push", "sound" => 'sound'), null, null); - $node = new ProtocolNode("iq", array( - "id" => $msgId, - "type" => "set", - "to" => static::WHATSAPP_SERVER - ), array($child), null); + $msgId = $this->createIqId(); + $child = new ProtocolNode('config', null, null, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'urn:xmpp:whatsapp:push', + 'type' => 'get', + 'to' => Constants::WHATSAPP_SERVER, + ], [$child], null); + $this->sendNode($node); - $this->waitForServer($msgId); } /** - * Send a request to return a list of groups user is currently participating - * in. + * Transfer your number to new one. + * + * @param string $number + * @param string $identity + */ + public function sendChangeNumber($number, $identity) + { + $msgId = $this->createIqId(); + + $usernameNode = new ProtocolNode('username', null, null, $number); + $passwordNode = new ProtocolNode('password', null, null, urldecode($identity)); + + $modifyNode = new ProtocolNode('modify', null, [$usernameNode, $passwordNode], null); + + $iqNode = new ProtocolNode('iq', + [ + 'xmlns' => 'urn:xmpp:whatsapp:account', + 'id' => $msgId, + 'type' => 'get', + 'to' => 'c.us', + ], [$modifyNode], null); + + $this->sendNode($iqNode); + } + + /** + * Send a request to return a list of groups user is currently participating in. * * To capture this list you will need to bind the "onGetGroups" event. */ public function sendGetGroups() { - $this->sendGetGroupsFiltered("participating"); + $this->sendGetGroupsFiltered('participating'); } /** - * Send a request to get information about a specific group + * Send a request to get new Groups V2 info. * - * @param string $gjid The specific group id + * @param $groupID + * The group JID */ - public function sendGetGroupsInfo($gjid) + public function sendGetGroupV2Info($groupID) { - $msgId = $this->createMsgId("getgroupinfo"); + $msgId = $this->nodeId['get_groupv2_info'] = $this->createIqId(); + + $queryNode = new ProtocolNode('query', + [ + 'request' => 'interactive', + ], null, null); + + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'w:g2', + 'type' => 'get', + 'to' => $this->getJID($groupID), + ], [$queryNode], null); - $child = new ProtocolNode("query", null, null, null); - $node = new ProtocolNode("iq", array( - "id" => $msgId, - "type" => "get", - "xmlns" => "w:g", - "to" => $this->getJID($gjid) - ), array($child), null); $this->sendNode($node); - $this->waitForServer($msgId); } /** - * Send a request to return a list of groups user has started - * in. - * - * To capture this list you will need to bind the "onGetGroups" event. + * Send a request to get a list of people you have currently blocked. */ - public function sendGetGroupsOwning() + public function sendGetPrivacyBlockedList() { - $this->sendGetGroupsFiltered("owning"); + $msgId = $this->nodeId['privacy'] = $this->createIqId(); + $child = new ProtocolNode('list', + [ + 'name' => 'default', + ], null, null); + + $child2 = new ProtocolNode('query', [], [$child], null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'jabber:iq:privacy', + 'type' => 'get', + ], [$child2], null); + + $this->sendNode($node); } /** - * Send a request to return a list of people participating in a specific - * group. - * - * @param string $gjid The specific group id + * Send a request to get privacy settings. */ - public function sendGetGroupsParticipants($gjid) + public function sendGetPrivacySettings() { - $msgId = $this->createMsgId("getgroupparticipants"); + $msgId = $this->nodeId['privacy_settings'] = $this->createIqId(); + $privacyNode = new ProtocolNode('privacy', null, null, null); + $node = new ProtocolNode('iq', + [ + 'to' => Constants::WHATSAPP_SERVER, + 'id' => $msgId, + 'xmlns' => 'privacy', + 'type' => 'get', + ], [$privacyNode], null); - $child = new ProtocolNode("list", null, null, null); - $node = new ProtocolNode("iq", array( - "id" => $msgId, - "type" => "get", - "xmlns" => "w:g", - "to" => $this->getJID($gjid) - ), array($child), null); $this->sendNode($node); - - $this->waitForServer($msgId); } /** - * Send a request to get a list of people you have currently blocked + * Set privacy of 'last seen', status or profile picture to all, contacts or none. + * + * @param string $category + * Options: 'last', 'status' or 'profile' + * @param string $value + * Options: 'all', 'contacts' or 'none' */ - public function sendGetPrivacyBlockedList() + public function sendSetPrivacySettings($category, $value) { - $msgId = $this->createMsgId("getprivacy"); - $child = new ProtocolNode("list", array( - "name" => "default" - ), null, null); - $child2 = new ProtocolNode("query", array( - "xmlns" => "jabber:iq:privacy" - ), array($child), null); - $node = new ProtocolNode("iq", array( - "id" => $msgId, - "type" => "get" - ), array($child2), null); + $msgId = $this->createIqId(); + $categoryNode = new ProtocolNode('category', + [ + 'name' => $category, + 'value' => $value, + ], null, null); + + $privacyNode = new ProtocolNode('privacy', null, [$categoryNode], null); + $node = new ProtocolNode('iq', + [ + 'to' => Constants::WHATSAPP_SERVER, + 'type' => 'set', + 'id' => $msgId, + 'xmlns' => 'privacy', + ], [$privacyNode], null); + $this->sendNode($node); - $this->waitForServer($msgId); } /** - * Get profile picture of specified user + * Get profile picture of specified user. * * @param string $number - * Number or JID of user - * - * @param bool $large - * Request large picture + * Number or JID of user + * @param bool $large + * Request large picture */ public function sendGetProfilePicture($number, $large = false) { - $hash = array(); - $hash["type"] = "image"; + $msgId = $this->nodeId['getprofilepic'] = $this->createIqId(); + + $hash = []; + $hash['type'] = 'image'; if (!$large) { - $hash["type"] = "preview"; + $hash['type'] = 'preview'; } - $picture = new ProtocolNode("picture", $hash, null, null); + $picture = new ProtocolNode('picture', $hash, null, null); + + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'type' => 'get', + 'xmlns' => 'w:profile:picture', + 'to' => $this->getJID($number), + ], [$picture], null); - $hash = array(); - $hash["id"] = $this->createMsgId("getpicture"); - $hash["type"] = "get"; - $hash["xmlns"] = "w:profile:picture"; - $hash["to"] = $this->getJID($number); - $node = new ProtocolNode("iq", $hash, array($picture), null); $this->sendNode($node); - $this->waitForServer($hash["id"]); } /** - * Request to retrieve the last online time of specific user. + * @param mixed $numbers Numbers to get profile profile photos of. * - * @param string $to - * Number or JID of user + * @return bool */ - public function sendGetRequestLastSeen($to) + public function sendGetProfilePhotoIds($numbers) { - $queryNode = new ProtocolNode("query", null, null, null); + if (!is_array($numbers)) { + $numbers = [$numbers]; + } - $messageHash = array(); - $messageHash["to"] = $this->getJID($to); - $messageHash["type"] = "get"; - $messageHash["id"] = $this->createMsgId("lastseen"); - $messageHash["xmlns"] = "jabber:iq:last"; + $msgId = $this->createIqId(); - $messageNode = new ProtocolNode("iq", $messageHash, array($queryNode), ""); - $this->sendNode($messageNode); - $this->waitForServer($messageHash["id"]); + $userNode = []; + for ($i = 0; $i < count($numbers); $i++) { + $userNode[$i] = new ProtocolNode('user', + [ + 'jid' => $this->getJID($numbers[$i]), + ], null, null); + } + + if (!count($userNode)) { + return false; + } + + $listNode = new ProtocolNode('list', null, $userNode, null); + + $iqNode = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'w:profile:picture', + 'type' => 'get', + ], [$listNode], null); + + $this->sendNode($iqNode); + + return true; } /** - * Send a request to get the current server properties + * Send a request to get the current server properties. */ public function sendGetServerProperties() { - $child = new ProtocolNode("props", null, null, null); - $node = new ProtocolNode("iq", array( - "id" => $this->createMsgId("getproperties"), - "type" => "get", - "xmlns" => "w", - "to" => "s.whatsapp.net" - ), array($child), null); + $id = $this->createIqId(); + $child = new ProtocolNode('props', null, null, null); + $node = new ProtocolNode('iq', + [ + 'id' => $id, + 'type' => 'get', + 'xmlns' => 'w', + 'to' => Constants::WHATSAPP_SERVER, + ], [$child], null); + $this->sendNode($node); } /** - * Get the current status message of a specific user. + * Send a request to get the current service pricing. * - * @param string[] $jids The users' JIDs + * @param string $lg + * Language + * @param string $lc + * Country */ - public function sendGetStatuses($jids) + public function sendGetServicePricing($lg, $lc) { - if(!is_array($jids)) - { - $jids = array($jids); - } - $children = array(); - foreach($jids as $jid) - { - $children[] = new ProtocolNode("user", array("jid" => $this->getJID($jid)), null, null); - } - $node = new ProtocolNode("iq", array( - "to" => "s.whatsapp.net", - "type" => "get", - "xmlns" => "status", - "id" => $this->createMsgId("getstatus") - ), array( - new ProtocolNode("status", null, $children, null) - ), null); + $msgId = $this->createIqId(); + $pricingNode = new ProtocolNode('pricing', + [ + 'lg' => $lg, + 'lc' => $lc, + ], null, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'urn:xmpp:whatsapp:account', + 'type' => 'get', + 'to' => Constants::WHATSAPP_SERVER, + ], [$pricingNode], null); + $this->sendNode($node); } /** - * Create a group chat. - * - * @param string $subject - * The group Subject - * @param array $participants - * An array with the participants numbers. + * Send a request to extend the account. + */ + public function sendExtendAccount() + { + $msgId = $this->createIqId(); + $extendingNode = new ProtocolNode('extend', null, null, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'urn:xmpp:whatsapp:account', + 'type' => 'set', + 'to' => Constants::WHATSAPP_SERVER, + ], [$extendingNode], null); + + $this->sendNode($node); + } + + /** + * Gets all the broadcast lists for an account. + */ + public function sendGetBroadcastLists() + { + $msgId = $this->nodeId['get_lists'] = $this->createIqId(); + $listsNode = new ProtocolNode('lists', null, null, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'w:b', + 'type' => 'get', + 'to' => Constants::WHATSAPP_SERVER, + ], [$listsNode], null); + + $this->sendNode($node); + } + + /** + * Send a request to get the normalized mobile number representing the JID. * - * @return string - * The group ID. + * @param string $countryCode Country Code + * @param string $number Mobile Number */ - public function sendGroupsChatCreate($subject, $participants = array()) + public function sendGetNormalizedJid($countryCode, $number) { - $groupHash = array(); - $groupHash["action"] = "create"; - $groupHash["subject"] = $subject; - $group = new ProtocolNode("group", $groupHash, null, ""); + $msgId = $this->createIqId(); + $ccNode = new ProtocolNode('cc', null, null, $countryCode); + $inNode = new ProtocolNode('in', null, null, $number); + $normalizeNode = new ProtocolNode('normalize', null, [$ccNode, $inNode], null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'urn:xmpp:whatsapp:account', + 'type' => 'get', + 'to' => Constants::WHATSAPP_SERVER, + ], [$normalizeNode], null); - $setHash = array(); - $setHash["id"] = $this->createMsgId("creategroup"); - $setHash["type"] = "set"; - $setHash["xmlns"] = "w:g"; - $setHash["to"] = static::WHATSAPP_GROUP_SERVER; - $groupNode = new ProtocolNode("iq", $setHash, array($group), ""); + $this->sendNode($node); + } - $this->sendNode($groupNode); - $this->waitForServer($setHash["id"]); - $groupId = $this->groupId; + /** + * Removes an account from WhatsApp. + * + * @param string $lg Language + * @param string $lc Country + * @param string $feedback User Feedback + */ + public function sendRemoveAccount($lg = null, $lc = null, $feedback = null) + { + $msgId = $this->createIqId(); + if ($feedback != null && strlen($feedback) > 0) { + if ($lg == null) { + $lg = ''; + } + + if ($lc == null) { + $lc = ''; + } - if (count($participants) > 0) { - $this->sendGroupsParticipantsAdd($groupId, $participants); + $child = new ProtocolNode('body', + [ + 'lg' => $lg, + 'lc' => $lc, + ], null, $feedback); + $childNode = [$child]; + } else { + $childNode = null; } - return $groupId; + $removeNode = new ProtocolNode('remove', null, $childNode, null); + $node = new ProtocolNode('iq', + [ + 'to' => Constants::WHATSAPP_SERVER, + 'xmlns' => 'urn:xmpp:whatsapp:account', + 'type' => 'get', + 'id' => $msgId, + ], [$removeNode], null); + + $this->sendNode($node); + $this->waitForServer($msgId); } - public function SendSetGroupSubject($gjid, $subject) + /** + * Send a ping to the server. + */ + public function sendPing() + { + $msgId = $this->createIqId(); + $pingNode = new ProtocolNode('ping', null, null, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'xmlns' => 'w:p', + 'type' => 'get', + 'to' => Constants::WHATSAPP_SERVER, + ], [$pingNode], null); + + $this->sendNode($node); + } + + /** + * Get the current status message of a specific user. + * + * @param mixed $jids The users' JIDs + */ + public function sendGetStatuses($jids) { - $child = new ProtocolNode("subject", array("value" => $subject), null, null); - $node = new ProtocolNode("iq", array( - "id" => $this->createMsgId("set_group_subject"), - "type" => "set", - "to" => $this->getJID($gjid), - "xmlns" => "w:g" - ), array($child), null); + if (!is_array($jids)) { + $jids = [$jids]; + } + + $children = []; + foreach ($jids as $jid) { + $children[] = new ProtocolNode('user', ['jid' => $this->getJID($jid)], null, null); + } + + $iqId = $this->nodeId['getstatuses'] = $this->createIqId(); + + $node = new ProtocolNode('iq', + [ + 'to' => Constants::WHATSAPP_SERVER, + 'type' => 'get', + 'xmlns' => 'status', + 'id' => $iqId, + ], [ + new ProtocolNode('status', null, $children, null), + ], null); + $this->sendNode($node); } /** - * End or delete a group chat + * Create a group chat. + * + * @param string $subject + * The group Subject + * @param array $participants + * An array with the participants numbers. * - * @param string $gjid The group ID + * @return string + * The group ID. */ - public function sendGroupsChatEnd($gjid) + public function sendGroupsChatCreate($subject, $participants) { - $gjid = $this->getJID($gjid); - $msgID = $this->createMsgId("endgroup"); + if (!is_array($participants)) { + $participants = [$participants]; + } + + $participantNode = []; + foreach ($participants as $participant) { + $participantNode[] = new ProtocolNode('participant', [ + 'jid' => $this->getJID($participant), + ], null, null); + } - $groupData = array(); - $groupData['id'] = $gjid; - $groupNode = new ProtocolNode('group', $groupData, null, null); + $id = $this->nodeId['groupcreate'] = $this->createIqId(); - $leaveData = array(); - $leaveData["action"] = "delete"; - $leaveNode = new ProtocolNode("leave", $leaveData, array($groupNode), null); + $createNode = new ProtocolNode('create', + [ + 'subject' => $subject, + ], $participantNode, null); - $iqData = array(); - $iqData["id"] = $msgID; - $iqData["type"] = "set"; - $iqData["xmlns"] = "w:g"; - $iqData["to"] = static::WHATSAPP_GROUP_SERVER; - $iqNode = new ProtocolNode("iq", $iqData, array($leaveNode), null); + $iqNode = new ProtocolNode('iq', + [ + 'xmlns' => 'w:g2', + 'id' => $id, + 'type' => 'set', + 'to' => Constants::WHATSAPP_GROUP_SERVER, + ], [$createNode], null); $this->sendNode($iqNode); - $this->waitForServer($msgID); + $this->waitForServer($id); + $groupId = $this->groupId; + + $this->eventManager()->fire('onGroupCreate', + [ + $this->phoneNumber, + $groupId, + ]); + + return $groupId; } /** - * Leave a group chat + * Change group's subject. * - * @param array $gjids An array of group IDs + * @param string $gjid The group id + * @param string $subject The subject + */ + public function sendSetGroupSubject($gjid, $subject) + { + $child = new ProtocolNode('subject', null, null, $subject); + $node = new ProtocolNode('iq', + [ + 'id' => $this->createIqId(), + 'type' => 'set', + 'to' => $this->getJID($gjid), + 'xmlns' => 'w:g2', + ], [$child], null); + + $this->sendNode($node); + } + + /** + * Leave a group chat. + * + * @param mixed $gjids Group or group's ID(s) */ public function sendGroupsLeave($gjids) { + $msgId = $this->nodeId['leavegroup'] = $this->createIqId(); + if (!is_array($gjids)) { - $gjids = array($this->getJID($gjids)); + $gjids = [$this->getJID($gjids)]; } - $nodes = array(); + + $nodes = []; foreach ($gjids as $gjid) { - $nodes[] = new ProtocolNode("group", array("id" => $this->getJID($gjid)), null, null); - } - $leave = new ProtocolNode("leave", array('action'=>'delete'), $nodes, null); - $hash = array(); - $hash["id"] = $this->createMsgId("leavegroups"); - $hash["to"] = static::WHATSAPP_GROUP_SERVER; - $hash["type"] = "set"; - $hash["xmlns"] = "w:g"; - $node = new ProtocolNode("iq", $hash, array($leave), null); + $nodes[] = new ProtocolNode('group', + [ + 'id' => $this->getJID($gjid), + ], null, null); + } + + $leave = new ProtocolNode('leave', + [ + 'action' => 'delete', + ], $nodes, null); + + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'to' => Constants::WHATSAPP_GROUP_SERVER, + 'type' => 'set', + 'xmlns' => 'w:g2', + ], [$leave], null); + $this->sendNode($node); - $this->waitForServer($hash["id"]); } /** * Add participant(s) to a group. * - * @param string $groupId - * The group ID. - * @param array $participants - * An array with the participants numbers to add + * @param string $groupId The group ID. + * @param string $participants An array with the participants numbers to add */ - public function sendGroupsParticipantsAdd($groupId, $participants) + public function sendGroupsParticipantsAdd($groupId, $participant) { - if(!is_array($participants)) { - $participants = array($participants); - } - $this->sendGroupsChangeParticipants($groupId, $participants, 'add'); + $msgId = $this->createMsgId(); + $this->sendGroupsChangeParticipants($groupId, $participant, 'add', $msgId); } /** - * Remove participant(s) from a group. + * Remove participant from a group. * - * @param string $groupId - * The group ID. - * @param array $participants - * An array with the participants numbers to remove + * @param string $groupId The group ID. + * @param string $participant The number of the participant you want to remove */ - public function sendGroupsParticipantsRemove($groupId, $participants) + public function sendGroupsParticipantsRemove($groupId, $participant) { - if(!is_array($participants)) { - $participants = array($participants); - } - $this->sendGroupsChangeParticipants($groupId, $participants, 'remove'); + $msgId = $this->createMsgId(); + $this->sendGroupsChangeParticipants($groupId, $participant, 'remove', $msgId); + } + + /** + * Promote participant of a group; Make a participant an admin of a group. + * + * @param string $gId The group ID. + * @param string $participant The number of the participant you want to promote + */ + public function sendPromoteParticipants($gId, $participant) + { + $msgId = $this->createMsgId(); + $this->sendGroupsChangeParticipants($gId, $participant, 'promote', $msgId); + } + + /** + * Demote participant of a group; remove participant of being admin of a group. + * + * @param string $gId The group ID. + * @param string $participant The number of the participant you want to demote + */ + public function sendDemoteParticipants($gId, $participant) + { + $msgId = $this->createMsgId(); + $this->sendGroupsChangeParticipants($gId, $participant, 'demote', $msgId); } /** * Send a text message to the user/group. * - * @param $to - * The recipient. - * @param string $txt - * The text message. - * @param $id + * @param string $to The recipient. + * @param string $txt The text message. + * @param bool $enc * - * @return string + * @return string Message ID. */ - public function sendMessage($to, $txt, $id = null) + public function sendMessage($to, $plaintext, $force_plain = false) { - $txt = $this->parseMessageForEmojis($txt); - $bodyNode = new ProtocolNode("body", null, null, $txt); - $id = $this->sendMessageNode($to, $bodyNode, $id); - $this->waitForServer($id); + if (extension_loaded('curve25519') && extension_loaded('protobuf') && !$force_plain) { + $to_num = ExtractNumber($to); + if (!(strpos($to, '-') !== false)) { + if (!$this->axolotlStore->containsSession($to_num, 1)) { + $this->sendGetCipherKeysFromUser($to_num); + } + + $sessionCipher = $this->getSessionCipher($to_num); + + if (in_array($to_num, $this->v2Jids) && !isset($this->v1Only[$to_num])) { + $version = '2'; + $alteredText = padMessage($plaintext); + } else { + $version = '1'; + $alteredText = $plaintext; + } + $cipherText = $sessionCipher->encrypt($alteredText); + + if ($cipherText instanceof WhisperMessage) { + $type = 'msg'; + } else { + $type = 'pkmsg'; + } + $message = $cipherText->serialize(); + $msgNode = new ProtocolNode('enc', + [ + 'v' => $version, + 'type' => $type, + ], null, $message); + } else { + /* if (in_array($to, $this->v2Jids)) + { + $version = "2"; + $plaintext = padMessage($plaintext); + } + else + $version = "1"; + + if(!$this->axolotlStore->containsSenderKey($to)){ + $gsb = new GroupSessionBuilder($this->axolotlStore); + $senderKey = $gsb->process ($groupId, $keyId, $iteration, $chainKey, $signatureKey) + } + $thi*/ + $msgNode = new ProtocolNode('body', null, null, $plaintext); + } + } else { + $msgNode = new ProtocolNode('body', null, null, $plaintext); + } + $plaintextNode = new ProtocolNode('body', null, null, $plaintext); + $id = $this->sendMessageNode($to, $msgNode, null, $plaintextNode); + + if ($this->messageStore !== null) { + $this->messageStore->saveMessage($this->phoneNumber, $to, $plaintext, $id, time()); + } + return $id; } /** - * Send audio to the user/group. * + * Send audio to the user/group. * - * @param $to - * The recipient. - * @param string $filepath - * The url/uri to the audio file. - * @param bool $storeURLmedia Keep copy of file - * @return bool + * @param string $to The recipient. + * @param string $filepath The url/uri to the audio file. + * @param bool $storeURLmedia Keep copy of file + * @param int $fsize + * @param string $fhash * + * @param bool $voice + * + * @return string|null Message ID if successfully, null if not. */ - public function sendMessageAudio($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = "") + public function sendMessageAudio($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = '', $voice = false) { - if ($fsize==0 || $fhash == "") - { - $allowedExtensions = array('3gp', 'caf', 'wav', 'mp3', 'wma', 'ogg', 'aif', 'aac', 'm4a'); - $size = 10 * 1024 * 1024; // Easy way to set maximum file size for this media type. - return $this->sendCheckAndSendMedia($filepath, $size, $to, 'audio', $allowedExtensions, $storeURLmedia); + $this->voice = $voice; + + if ($fsize != 0 && $fhash != '') { + return $this->sendRequestFileUpload($fhash, 'audio', $fsize, $filepath, $to); } - else{ - $this->sendRequestFileUpload($fhash, 'audio', $fsize, $filepath, $to); - return true; - } + + $allowedExtensions = ['3gp', 'caf', 'wav', 'mp3', 'wma', 'ogg', 'aif', 'aac', 'm4a']; + $size = 10 * 1024 * 1024; // Easy way to set maximum file size for this media type. + // Return message ID. Make pull request for this. + return $this->sendCheckAndSendMedia($filepath, $size, $to, 'audio', $allowedExtensions, $storeURLmedia); } /** * Send the composing message status. When typing a message. * - * @param string $to - * The recipient to send status to. + * @param string $to The recipient to send status to. */ public function sendMessageComposing($to) { - $this->sendChatState($to, "composing"); + $this->sendChatState($to, 'composing'); } /** - * Send an image file to group/user + * Send an image file to group/user. * - * @param string $to - * Recipient number - * @param string $filepath - * The url/uri to the image file. - * @param bool $storeURLmedia Keep copy of file - * @param int $fsize size of the media file - * @param string $fhash base64 hash of the media file + * @param string $to Recipient number + * @param string $filepath The url/uri to the image file. + * @param bool $storeURLmedia Keep copy of file + * @param int $fsize size of the media file + * @param string $fhash base64 hash of the media file + * @param string $caption * - * @return bool + * @return string|null Message ID if successfully, null if not. */ - public function sendMessageImage($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = "") + public function sendMessageImage($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = '', $caption = '', $encrypted = false) { - if ($fsize==0 || $fhash == "") - { - $allowedExtensions = array('jpg', 'jpeg', 'gif', 'png'); - $size = 5 * 1024 * 1024; // Easy way to set maximum file size for this media type. - return $this->sendCheckAndSendMedia($filepath, $size, $to, 'image', $allowedExtensions, $storeURLmedia); + if ($fsize != 0 && $fhash != '' && !$encrypted) { + return $this->sendRequestFileUpload($fhash, 'image', $fsize, $filepath, $to, $caption); } - else{ - $this->sendRequestFileUpload($fhash, 'image', $fsize, $filepath, $to); - return true; - } + + $allowedExtensions = ['jpg', 'jpeg', 'gif', 'png']; + $size = 5 * 1024 * 1024; // Easy way to set maximum file size for this media type. + // Return message ID. Make pull request for this. + return $this->sendCheckAndSendMedia($filepath, $size, $to, 'image', $allowedExtensions, $storeURLmedia, $caption); } /** * Send a location to the user/group. * - * If no name is supplied , receiver will see large sized google map - * thumbnail of entered Lat/Long but NO name/url for location. + * If no name is supplied, the receiver will see a large google maps thumbnail of the lat/long, + * but NO name or url of the location. * - * With name supplied, a combined map thumbnail/name box is displayed + * When a name supplied, a combined map thumbnail/name box is displayed. * - * @param array|string $to The recipient(s) to send to. - * @param float $long The longitude of the location eg 54.31652 - * @param float $lat The latitude if the location eg -6.833496 - * @param string $name (Optional) The custom name you would like to give this location. - * @param string $url (Optional) A URL to attach to the location. + * @param mixed $to The recipient(s) to send the location to. + * @param float $long The longitude of the location, e.g. 54.31652. + * @param float $lat The latitude of the location, e.g. -6.833496. + * @param string $name (Optional) A custom name for the specified location. + * @param string $url (Optional) A URL to attach to the specified location. + * + * @return string Message ID */ public function sendMessageLocation($to, $long, $lat, $name = null, $url = null) { - $mediaHash = array(); - $mediaHash['xmlns'] = "urn:xmpp:whatsapp:mms"; - $mediaHash['type'] = "location"; - $mediaHash['latitude'] = $lat; - $mediaHash['longitude'] = $long; - $mediaHash['name'] = $name; - $mediaHash['url'] = $url; + $mediaNode = new ProtocolNode('media', + [ + 'type' => 'location', + 'encoding' => 'raw', + 'latitude' => $lat, + 'longitude' => $long, + 'name' => $name, + 'url' => $url, + ], null, null); - $mediaNode = new ProtocolNode("media", $mediaHash, null, null); + $id = (is_array($to)) ? $this->sendBroadcast($to, $mediaNode, 'media') : $this->sendMessageNode($to, $mediaNode); - if (is_array($to)) { - $id = $this->sendBroadcast($to, $mediaNode, "media"); - } else { - $id = $this->sendMessageNode($to, $mediaNode); - } - $this->waitForServer($id); + //$this->waitForServer($id); + + // Return message ID. Make pull request for this. + return $id; } /** * Send the 'paused composing message' status. * - * @param string $to - * The recipient number or ID. + * @param string $to The recipient number or ID. */ public function sendMessagePaused($to) { - $this->sendChatState($to, "paused"); + $this->sendChatState($to, 'paused'); } protected function sendChatState($to, $state) { - $node = new ProtocolNode("chatstate", array("to" => $this->getJID($to)), array(new ProtocolNode($state, null, null, null)), null); + $node = new ProtocolNode('chatstate', + [ + 'to' => $this->getJID($to), + ], [new ProtocolNode($state, null, null, null)], null); + $this->sendNode($node); } /** * Send a video to the user/group. * - * @param string $to - * The recipient to send. - * @param string $filepath - * The url/uri to the MP4/MOV video. - * @param bool $storeURLmedia Keep a copy of media file. - * @param int $fsize size of the media file - * @param string $fhash base64 hash of the media file + * @param string $to The recipient to send. + * @param string $filepath A URL/URI to the MP4/MOV video. + * @param bool $storeURLmedia Keep a copy of media file. + * @param int $fsize Size of the media file + * @param string $fhash base64 hash of the media file + * @param string $caption * * - * @return bool + * @return string|null Message ID if successfully, null if not. */ - public function sendMessageVideo($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = "") + public function sendMessageVideo($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = '', $caption = '') { - if ($fsize==0 || $fhash == "") - { - $allowedExtensions = array('3gp', 'mp4', 'mov', 'avi'); - $size = 20 * 1024 * 1024; // Easy way to set maximum file size for this media type. - return $this->sendCheckAndSendMedia($filepath, $size, $to, 'video', $allowedExtensions, $storeURLmedia); + if ($fsize != 0 && $fhash != '') { + return $this->sendRequestFileUpload($fhash, 'video', $fsize, $filepath, $to, $caption); } - else{ - $this->sendRequestFileUpload($fhash, 'video', $fsize, $filepath, $to); - return true; - } + + $allowedExtensions = ['3gp', 'mp4', 'mov', 'avi']; + $size = 20 * 1024 * 1024; // Easy way to set maximum file size for this media type. + // Return message ID. Make pull request for this. + return $this->sendCheckAndSendMedia($filepath, $size, $to, 'video', $allowedExtensions, $storeURLmedia, $caption); } /** @@ -1193,80 +1505,79 @@ public function sendNextMessage() */ public function sendOfflineStatus() { - $messageNode = new ProtocolNode("presence", array("type" => "inactive"), null, ""); + $messageNode = new ProtocolNode('presence', ['type' => 'unavailable'], null, ''); $this->sendNode($messageNode); } /** - * Send a pong to the whatsapp server. I'm alive! + * Send a pong to the WhatsApp server. I'm alive! * - * @param string $msgid - * The id of the message. + * @param string $msgid The id of the message. */ public function sendPong($msgid) { - $messageHash = array(); - $messageHash["to"] = static::WHATSAPP_SERVER; - $messageHash["id"] = $msgid; - $messageHash["type"] = "result"; + $messageNode = new ProtocolNode('iq', + [ + 'to' => Constants::WHATSAPP_SERVER, + 'id' => $msgid, + 'type' => 'result', + ], null, ''); - $messageNode = new ProtocolNode("iq", $messageHash, null, ""); $this->sendNode($messageNode); - $this->eventManager()->fireSendPong( - $this->phoneNumber, - $msgid - ); + $this->eventManager()->fire('onSendPong', + [ + $this->phoneNumber, + $msgid, + ]); } public function sendAvailableForChat($nickname = null) { - $presence = array(); - if($nickname) - { + $presence = []; + if ($nickname) { //update nickname $this->name = $nickname; } + $presence['name'] = $this->name; - $node = new ProtocolNode("presence", $presence, null, ""); + $presence['type'] = 'available'; + $node = new ProtocolNode('presence', $presence, null, ''); $this->sendNode($node); + $this->eventManager()->fire('onSendPresence', + [ + $this->phoneNumber, + $presence['type'], + $this->name, + ]); } /** - * Send presence status. + * Send presence subscription, automatically receive presence updates as long as the socket is open. * - * @param string $type - * The presence status. + * @param string $to Phone number. */ - public function sendPresence($type = "active") + public function sendPresenceSubscription($to) { - $presence = array(); - $presence['type'] = $type; - $node = new ProtocolNode("presence", $presence, null, ""); + $node = new ProtocolNode('presence', ['type' => 'subscribe', 'to' => $this->getJID($to)], null, ''); $this->sendNode($node); - $this->eventManager()->fireSendPresence( - $this->phoneNumber, - $presence['type'], - $this->name - ); } /** - * Send presence subscription, automatically receive presence updates as long as the socket is open. + * Unsubscribe, will stop subscription. * - * @param string $to - * Phone number. + * @param string $to Phone number. */ - public function sendPresenceSubscription($to) + public function sendPresenceUnsubscription($to) { - $node = new ProtocolNode("presence", array("type" => "subscribe", "to" => $this->getJID($to)), null, ""); + $node = new ProtocolNode('presence', ['type' => 'unsubscribe', 'to' => $this->getJID($to)], null, ''); $this->sendNode($node); } /** - * Set the picture for the group + * Set the picture for the group. * - * @param string $gjid The groupID - * @param string $path The URL/URI of the image to use + * @param string $gjid The groupID + * @param string $path The URL/URI of the image to use */ public function sendSetGroupPicture($gjid, $path) { @@ -1276,201 +1587,213 @@ public function sendSetGroupPicture($gjid, $path) /** * Set the list of numbers you wish to block receiving from. * - * @param array $blockedJids Array of numbers to block messages from. + * @param mixed $blockedJids One or more numbers to block messages from. */ - public function sendSetPrivacyBlockedList($blockedJids = array()) + public function sendSetPrivacyBlockedList($blockedJids = []) { if (!is_array($blockedJids)) { - $blockedJids = array($blockedJids); + $blockedJids = [$blockedJids]; } - $items = array(); + + $items = []; foreach ($blockedJids as $index => $jid) { - $item = new ProtocolNode("item", array( - "type" => "jid", - "value" => $this->getJID($jid), - "action" => "deny", - "order" => $index + 1//WhatsApp stream crashes on zero index - ), null, null); + $item = new ProtocolNode('item', + [ + 'type' => 'jid', + 'value' => $this->getJID($jid), + 'action' => 'deny', + 'order' => $index + 1, //WhatsApp stream crashes on zero index + ], null, null); $items[] = $item; } - $child = new ProtocolNode("list", array("name" => "default"), $items, null); - $child2 = new ProtocolNode("query", array("xmlns" => "jabber:iq:privacy"), array($child), null); - $node = new ProtocolNode("iq", array( - "id" => $this->createMsgId("setprivacy"), - "type" => "set" - ), array($child2), null); + + $child = new ProtocolNode('list', + [ + 'name' => 'default', + ], $items, null); + + $child2 = new ProtocolNode('query', null, [$child], null); + $node = new ProtocolNode('iq', + [ + 'id' => $this->createIqId(), + 'xmlns' => 'jabber:iq:privacy', + 'type' => 'set', + ], [$child2], null); + $this->sendNode($node); } /** - * Set your profile picture + * Set your profile picture. * - * @param string $path URL/URI of image + * @param string $path URL/URI of image */ public function sendSetProfilePicture($path) { $this->sendSetPicture($this->phoneNumber, $path); } + /* + * Removes the profile photo. + */ + + public function sendRemoveProfilePicture() + { + $msgId = $this->createIqId(); + + $picture = new ProtocolNode('picture', null, null, null); + + $thumb = new ProtocolNode('picture', + [ + 'type' => 'preview', + ], null, null); + + $node = new ProtocolNode('iq', + [ + 'id' => $msgId, + 'to' => $this->getJID($this->phoneNumber), + 'type' => 'set', + 'xmlns' => 'w:profile:picture', + ], [$picture, $thumb], null); + + $this->sendNode($node); + } + /** - * Set the recovery token for your account to allow you to - * retrieve your password at a later stage. - * @param string $token A user generated token. + * Set the recovery token for your account to allow you to retrieve your password at a later stage. + * + * @param string $token A user generated token. */ public function sendSetRecoveryToken($token) { - $child = new ProtocolNode("pin", array("xmlns" => "w:ch:p"), null, $token); - $node = new ProtocolNode("iq", array( - "id" => $this->createMsgId("settoken"), - "type" => "set", - "to" => "s.whatsapp.net" - ), array($child), null); + $child = new ProtocolNode('pin', + [ + 'xmlns' => 'w:ch:p', + ], null, $token); + + $node = new ProtocolNode('iq', + [ + 'id' => $this->createIqId(), + 'type' => 'set', + 'to' => Constants::WHATSAPP_SERVER, + ], [$child], null); + $this->sendNode($node); } /** * Update the user status. * - * @param string $txt - * The text of the message status to send. + * @param string $txt The text of the message status to send. */ public function sendStatusUpdate($txt) { - $child = new ProtocolNode("status", null, null, $txt); - $node = new ProtocolNode("iq", array( - "to" => "s.whatsapp.net", - "type" => "set", - "id" => $this->createMsgId("sendstatus"), - "xmlns" => "status" - ), array($child), null); + $child = new ProtocolNode('status', null, null, $txt); + $nodeID = $this->createIqId(); + $node = new ProtocolNode('iq', + [ + 'to' => Constants::WHATSAPP_SERVER, + 'type' => 'set', + 'id' => $nodeID, + 'xmlns' => 'status', + ], [$child], null); $this->sendNode($node); - $this->eventManager()->fireSendStatusUpdate( - $this->phoneNumber, - $txt - ); + $this->eventManager()->fire('onSendStatusUpdate', + [ + $this->phoneNumber, + $txt, + ]); } /** * Send a vCard to the user/group. * - * @param $to - * The recipient to send. - * @param $name - * The contact name. - * @param $vCard - * The contact vCard to send. + * @param string $to The recipient to send. + * @param string $name The contact name. + * @param object $vCard The contact vCard to send. + * + * @return string Message ID */ public function sendVcard($to, $name, $vCard) { - $vCardAttribs = array(); - $vCardAttribs['name'] = $name; - $vCardNode = new ProtocolNode("vcard", $vCardAttribs, null, $vCard); + $vCardNode = new ProtocolNode('vcard', + [ + 'name' => $name, + ], null, $vCard); - $mediaAttribs = array(); - $mediaAttribs["type"] = "vcard"; + $mediaNode = new ProtocolNode('media', + [ + 'type' => 'vcard', + ], [$vCardNode], ''); - $mediaNode = new ProtocolNode("media", $mediaAttribs, array($vCardNode), ""); - $this->sendMessageNode($to, $mediaNode); + // Return message ID. Make pull request for this. + return $this->sendMessageNode($to, $mediaNode); } /** * Send a vCard to the user/group as Broadcast. * - * @param $targets - * Array of recipients to send. - * @param $name - * The vCard contact name. - * @param $vCard - * The contact vCard to send. + * @param array $targets An array of recipients to send to. + * @param string $name The vCard contact name. + * @param object $vCard The contact vCard to send. + * + * @return string Message ID */ - public function sendBroadcastVcard($targets, $name, $vCard) + public function sendBroadcastVcard($targets, $name, $vCard) { - $vCardAttribs = array(); - $vCardAttribs['name'] = $name; - $vCardNode = new ProtocolNode("vcard", $vCardAttribs, null, $vCard); - - $mediaAttribs = array(); - $mediaAttribs["type"] = "vcard"; + $vCardNode = new ProtocolNode('vcard', + [ + 'name' => $name, + ], null, $vCard); - $mediaNode = new ProtocolNode("media", $mediaAttribs, array($vCardNode), ""); - $this->sendBroadcast($targets, $mediaNode, "media"); - } + $mediaNode = new ProtocolNode('media', + [ + 'type' => 'vcard', + ], [$vCardNode], ''); - /** - * Sets the bind of the new message. - */ - public function setNewMessageBind($bind) - { - $this->newMsgBind = $bind; + // Return message ID. Make pull request for this. + return $this->sendBroadcast($targets, $mediaNode, 'media'); } /** - * Upload file to WhatsApp servers. - * - * @param $file - * The uri of the file. + * Rejects a call. * - * @return string|bool - * Return the remote url or false on failure. + * @param array $to Phone number. + * @param string $id The main node id + * @param string $callId The call-id */ - public function uploadFile($file) + public function rejectCall($to, $id, $callId) { - $data['file'] = "@" . $file; - $ch = curl_init(); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); - curl_setopt($ch, CURLOPT_URL, static::WHATSAPP_UPLOAD_HOST); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - $response = curl_exec($ch); - curl_close($ch); + $rejectNode = new ProtocolNode('reject', + [ + 'call-id' => $callId, + ], null, null); - $xml = simplexml_load_string($response); - $url = strip_tags($xml->dict->string[3]->asXML()); + $callNode = new ProtocolNode('call', + [ + 'id' => $id, + 'to' => $this->getJID($to), + ], [$rejectNode], null); - if (!empty($url)) { - $this->eventManager()->fireUploadFile( - $this->phoneNumber, - basename($file), - $url - ); - return $url; - } else { - $this->eventManager()->fireUploadFileFailed( - $this->phoneNumber, - basename($file) - ); - return false; - } + $this->sendNode($callNode); } /** - * Wait for message delivery notification. + * Sets the bind of the new message. + * + * @param $bind */ - public function waitForMessageReceipt() + public function setNewMessageBind($bind) { - $received = false; - do { - $this->pollMessage(); - $msgs = $this->getMessages(); - foreach ($msgs as $m) { - // Process inbound messages. - if ($m->getTag() == "message") { - if ($m->getChild('received') != null && $m->getAttribute('retry') != null) { - $received = true; - } elseif ($m->getChild('received') != null && $m->getAttribute('retry') != null) { - throw new Exception('There was a problem trying to send the message, please retry.'); - } - } - } - } while (!$received); + $this->newMsgBind = $bind; } /** - * Wait for Whatsapp server to acknowledge *it* has received message. - * @param string $id The id of the node sent that we are awaiting acknowledgement of. + * Wait for WhatsApp server to acknowledge *it* has received message. + * + * @param string $id The id of the node sent that we are awaiting acknowledgement of. + * @param int $timeout */ public function waitForServer($id, $timeout = 5) { @@ -1482,436 +1805,262 @@ public function waitForServer($id, $timeout = 5) } /** - * Authenticate with the Whatsapp Server. - * - * @return String - * Returns binary string - */ - protected function authenticate() - { - $keys = KeyStream::GenerateKeys(base64_decode($this->password), $this->challengeData); - $this->inputKey = new KeyStream($keys[2], $keys[3]); - $this->outputKey = new KeyStream($keys[0], $keys[1]); - $phone = $this->dissectPhone(); - $array = "\0\0\0\0" . $this->phoneNumber . $this->challengeData;// . time() . static::WHATSAPP_USER_AGENT . " MccMnc/" . str_pad($phone["mcc"], 3, "0", STR_PAD_LEFT) . "001"; - $response = $this->outputKey->EncodeMessage($array, 0, 4, strlen($array) - 4); - return $response; - } - - /** - * Add the authentication nodes. + * Create a unique msg id. * - * @return ProtocolNode - * Return itself. + * @return string + * A message id string. */ - protected function createAuthNode() - { - $authHash = array(); - $authHash["xmlns"] = "urn:ietf:params:xml:ns:xmpp-sasl"; - $authHash["mechanism"] = "WAUTH-2"; - $authHash["user"] = $this->phoneNumber; - $data = $this->createAuthBlob(); - $node = new ProtocolNode("auth", $authHash, null, $data); - - return $node; - } - - protected function createAuthBlob() + protected function createMsgId() { - if($this->challengeData) { - $key = wa_pbkdf2('sha1', base64_decode($this->password), $this->challengeData, 16, 20, true); - $this->inputKey = new KeyStream($key[2], $key[3]); - $this->outputKey = new KeyStream($key[0], $key[1]); - $this->reader->setKey($this->inputKey); - //$this->writer->setKey($this->outputKey); - $phone = $this->dissectPhone(); - $array = "\0\0\0\0" . $this->phoneNumber . $this->challengeData . time() . static::WHATSAPP_USER_AGENT . " MccMnc/" . str_pad($phone["mcc"], 3, "0", STR_PAD_LEFT) . "001"; - $this->challengeData = null; - return $this->outputKey->EncodeMessage($array, 0, strlen($array), false); + $msg = hex2bin($this->messageId); + $chars = str_split($msg); + $chars_val = array_map('ord', $chars); + $pos = count($chars_val) - 1; + while (true) { + if ($chars_val[$pos] < 255) { + $chars_val[$pos]++; + break; + } else { + $chars_val[$pos] = 0; + $pos--; + } } - return null; - } - - /** - * Add the auth response to protocoltreenode. - * - * @return ProtocolNode - * Return itself. - */ - protected function createAuthResponseNode() - { - $resp = $this->authenticate(); - $respHash = array(); - $respHash["xmlns"] = "urn:ietf:params:xml:ns:xmpp-sasl"; - $node = new ProtocolNode("response", $respHash, null, $resp); - - return $node; - } - - /** - * Add stream features. - * @param bool $profileSubscribe - * - * @return ProtocolNode - * Return itself. - */ - protected function createFeaturesNode() - { - $parent = new ProtocolNode("stream:features", null, null, null); + $chars = array_map('chr', $chars_val); + $msg = bin2hex(implode($chars)); + $this->messageId = $msg; - return $parent; + return $this->messageId; } /** - * Create a unique msg id. + * iq id. * - * @param string $prefix * @return string - * A message id string. + * Iq id */ - protected function createMsgId($prefix) + public function createIqId() { - $msgid = "$prefix-" . time() . '-' . $this->messageCounter; - $this->messageCounter++; + $iqId = $this->iqCounter; + $this->iqCounter++; + $id = dechex($iqId); - return $msgid; + return $id; } /** * Print a message to the debug console. * - * @param string $debugMsg - * The debug message. + * @param mixed $debugMsg The debug message. + * + * @return bool */ - protected function debugPrint($debugMsg) + public function debugPrint($debugMsg) { if ($this->debug) { - echo $debugMsg; + if (is_array($debugMsg) || is_object($debugMsg)) { + print_r($debugMsg); + } else { + echo $debugMsg; + } + + return true; } + + return false; } - /** - * Dissect country code from phone number. - * - * @return array - * An associative array with country code and phone number. - * - country: The detected country name. - * - cc: The detected country code (phone prefix). - * - phone: The phone number. - * - ISO3166: 2-Letter country code - * - ISO639: 2-Letter language code - * Return false if country code is not found. - */ - protected function dissectPhone() + public function logFile($tag, $message, $context = []) { - if (($handle = fopen(dirname(__FILE__).'/countries.csv', 'rb')) !== false) { - while (($data = fgetcsv($handle, 1000)) !== false) { - if (strpos($this->phoneNumber, $data[1]) === 0) { - // Return the first appearance. - fclose($handle); - - $mcc = explode("|", $data[2]); - $mcc = $mcc[0]; - - //hook: - //fix country code for North America - if(substr($data[1], 0, 1) == "1") - { - $data[1] = "1"; - } - - $phone = array( - 'country' => $data[0], - 'cc' => $data[1], - 'phone' => substr($this->phoneNumber, strlen($data[1]), strlen($this->phoneNumber)), - 'mcc' => $mcc, - 'ISO3166' => @$data[3], - 'ISO639' => @$data[4] - ); - - $this->eventManager()->fireDissectPhone( - $this->phoneNumber, - $phone['country'], - $phone['cc'], - $phone['phone'], - $phone['mcc'], - $phone['ISO3166'], - $phone['ISO639'] - ); - - return $phone; - } - } - fclose($handle); + if ($this->log) { + $this->logger->log($tag, $message, $context); } - - $this->eventManager()->fireDissectPhoneFailed( - $this->phoneNumber - ); - - return false; } /** - * Send the nodes to the Whatsapp server to log in. + * Have we an active connection with WhatsAPP AND a valid login already? * - * @param bool $profileSubscribe - * Set this to true if you would like Whatsapp to send a - * notification to your phone when one of your contacts - * changes/update their picture. + * @return bool */ - protected function doLogin() + public function isLoggedIn() { - $this->writer->resetKey(); - $this->reader->resetKey(); - $resource = static::WHATSAPP_DEVICE . '-' . static::WHATSAPP_VER . '-' . static::PORT; - $data = $this->writer->StartStream(static::WHATSAPP_SERVER, $resource); - $feat = $this->createFeaturesNode(); - $auth = $this->createAuthNode(); - $this->sendData($data); - $this->sendNode($feat); - $this->sendNode($auth); + //If you aren't connected you can't be logged in! ($this->isConnected()) + //We are connected - but are we logged in? (the rest) + return $this->isConnected() && !empty($this->loginStatus) && $this->loginStatus === Constants::CONNECTED_STATUS; + } - $this->pollMessage(); - $this->pollMessage(); - $this->pollMessage(); + public function sendSync($numbers, $deletedNumbers = null, $syncType = 3) + { + $users = []; + if (!is_array($numbers)) { + $numbers = [$numbers]; + } - if($this->challengeData != null) { - $data = $this->createAuthResponseNode(); - $this->sendNode($data); - $this->reader->setKey($this->inputKey); - $this->writer->setKey($this->outputKey); - $this->pollMessage(); + for ($i = 0; $i < count($numbers); $i++) { // number must start with '+' if international contact + $users[$i] = new ProtocolNode('user', null, null, (substr($numbers[$i], 0, 1) != '+') ? ('+'.$numbers[$i]) : ($numbers[$i])); } - if(strcmp($this->loginStatus, static::DISCONNECTED_STATUS) == 0) - { - throw new Exception('Login Failure'); - } - else - { - $this->eventManager()->fireLogin( - $this->phoneNumber - ); - $this->sendAvailableForChat(); - } - } + if (!is_null($deletedNumbers)) { + if (!is_array($deletedNumbers)) { + $deletedNumbers = [$deletedNumbers]; + } + for ($j = 0; $j < count($deletedNumbers); $j++, $i++) { + $users[$i] = new ProtocolNode('user', ['jid' => $this->getJID($deletedNumbers[$j]), 'type' => 'delete'], null, null); + } + } - /** - * Create an identity string - * - * @param string $identity File name where identity is going to be saved. - * @return string Correctly formatted identity - */ - protected function buildIdentity($identity) - { - if (file_exists($identity.".dat")) - { - return file_get_contents($identity.'.dat'); - } - else - { - $id = fopen($identity.".dat", "w"); - $bytes = "%".implode("%", str_split(strtoupper(bin2hex(openssl_random_pseudo_bytes(16))), 2)); - fwrite($id, $bytes); - fclose($id); + switch ($syncType) { + case 0: + $mode = 'full'; + $context = 'registration'; + break; + case 1: + $mode = 'full'; + $context = 'interactive'; + break; + case 2: + $mode = 'full'; + $context = 'background'; + break; + case 3: + $mode = 'delta'; + $context = 'interactive'; + break; + case 4: + $mode = 'delta'; + $context = 'background'; + break; + case 5: + $mode = 'query'; + $context = 'interactive'; + break; + case 6: + $mode = 'chunked'; + $context = 'registration'; + break; + case 7: + $mode = 'chunked'; + $context = 'interactive'; + break; + case 8: + $mode = 'chunked'; + $context = 'background'; + break; + default: + $mode = 'delta'; + $context = 'background'; + } + + $id = $this->createIqId(); + + $node = new ProtocolNode('iq', + [ + 'id' => $id, + 'xmlns' => 'urn:xmpp:whatsapp:sync', + 'type' => 'get', + ], [ + new ProtocolNode('sync', + [ + 'mode' => $mode, + 'context' => $context, + 'sid' => 'sync_sid_full_'.sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)), + 'index' => '0', + 'last' => 'true', + ], $users, null), + ], null); + + $this->sendNode($node); + $this->waitForServer($id); - return $bytes; - } + return $id; } - protected function checkIdentity($identity) + public function setMessageStore(MessageStoreInterface $messageStore) { - if (file_exists($identity.".dat")) - { - return (strlen(file_get_contents($identity.'.dat')) == 48); - } - else - { - return false; - } + $this->messageStore = $messageStore; } - public function sendSync(array $numbers, $mode = "full", $context = "registration", $index = 0, $last = true) + public function setAxolotlStore(axolotlInterface $axolotlStore) { - $users = array(); - foreach ($numbers as $number) { // number must start with '+' if international contact - $users[] = new ProtocolNode("user", null, null, (substr($number, 0, 1) != '+')?('+' . $number):($number)); - } - - $id = $this->createMsgId("sendsync_"); - - $node = new ProtocolNode("iq", array( - "to" => $this->getJID($this->phoneNumber), - "type" => "get", - "id" => $id, - "xmlns" => "urn:xmpp:whatsapp:sync" - ), array( - new ProtocolNode("sync", array( - "mode" => $mode, - "context" => $context, - "sid" => "".((time() + 11644477200) * 10000000), - "index" => "".$index, - "last" => $last ? "true" : "false" - ), $users, null) - ), null); - - $this->sendNode($node); - - return $id; + $this->axolotlStore = $axolotlStore; } /** - * Process number/jid and turn it into a JID if necessary + * Process number/jid and turn it into a JID if necessary. * * @param string $number - * Number to process + * Number to process + * * @return string */ - protected function getJID($number) + public function getJID($number) { if (!stristr($number, '@')) { //check if group message if (stristr($number, '-')) { //to group - $number .= "@" . static::WHATSAPP_GROUP_SERVER; + $number .= '@'.Constants::WHATSAPP_GROUP_SERVER; } else { //to normal user - $number .= "@" . static::WHATSAPP_SERVER; + $number .= '@'.Constants::WHATSAPP_SERVER; } } return $number; } - - /** - * Retrieves media file and info from either a URL or localpath + * Retrieves media file and info from either a URL or localpath. * - * @param $filepath - * The URL or path to the mediafile you wish to send - * @param $maxsizebytes - * The maximum size in bytes the media file can be. Default 1MB + * @param string $filepath The URL or path to the mediafile you wish to send + * @param int $maxsizebytes The maximum size in bytes the media file can be. Default 5MB * - * @return bool false if file information can not be obtained. + * @return bool false if file information can not be obtained. */ - protected function getMediaFile($filepath, $maxsizebytes = 1048576) + protected function getMediaFile($filepath, $maxsizebytes = 5242880) { if (filter_var($filepath, FILTER_VALIDATE_URL) !== false) { - $this->mediaFileInfo = array(); + $this->mediaFileInfo = []; $this->mediaFileInfo['url'] = $filepath; - //File is a URL. Create a curl connection but DON'T download the body content - //because we want to see if file is too big. - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, "$filepath"); - curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11"); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_HEADER, false); - curl_setopt($curl, CURLOPT_NOBODY, true); - - if (curl_exec($curl) === false) { - return false; - } - - //While we're here, get mime type and filesize and extension - $info = curl_getinfo($curl); - $this->mediaFileInfo['filesize'] = $info['download_content_length']; - $this->mediaFileInfo['filemimetype'] = $info['content_type']; - $this->mediaFileInfo['fileextension'] = pathinfo(parse_url($this->mediaFileInfo['url'], PHP_URL_PATH), PATHINFO_EXTENSION); + $media = file_get_contents($filepath); + $this->mediaFileInfo['filesize'] = strlen($media); - //Only download file if it's not too big - //TODO check what max file size whatsapp server accepts. if ($this->mediaFileInfo['filesize'] < $maxsizebytes) { - //Create temp file in media folder. Media folder must be writable! - $this->mediaFileInfo['filepath'] = tempnam(getcwd() . '/' . static::MEDIA_FOLDER, 'WHA'); - $fp = fopen($this->mediaFileInfo['filepath'], 'w'); - if ($fp) { - curl_setopt($curl, CURLOPT_NOBODY, false); - curl_setopt($curl, CURLOPT_BUFFERSIZE, 1024); - curl_setopt($curl, CURLOPT_FILE, $fp); - curl_exec($curl); - fclose($fp); - } else { - unlink($this->mediaFileInfo['filepath']); - curl_close($curl); - return false; - } - //Success - curl_close($curl); + $this->mediaFileInfo['filepath'] = tempnam($this->dataFolder.Constants::MEDIA_FOLDER, 'WHA'); + file_put_contents($this->mediaFileInfo['filepath'], $media); + $this->mediaFileInfo['filemimetype'] = get_mime($this->mediaFileInfo['filepath']); + $this->mediaFileInfo['fileextension'] = getExtensionFromMime($this->mediaFileInfo['filemimetype']); + return true; } else { - //File too big. Don't Download. - curl_close($curl); return false; } - } else if (file_exists($filepath)) { + } elseif (file_exists($filepath)) { //Local file $this->mediaFileInfo['filesize'] = filesize($filepath); if ($this->mediaFileInfo['filesize'] < $maxsizebytes) { $this->mediaFileInfo['filepath'] = $filepath; $this->mediaFileInfo['fileextension'] = pathinfo($filepath, PATHINFO_EXTENSION); - //TODO - //Get Mime type using finfo. -// $finfo = new finfo_open(FILEINFO_MIME_TYPE); -// $this->_mediafileinfo['filemimetype'] = finfo_file($finfo, $filepath); -// finfo_close($finfo); - //mime_content_type deprecated - $this->mediaFileInfo['filemimetype'] = mime_content_type($filepath); + $this->mediaFileInfo['filemimetype'] = get_mime($filepath); + return true; } else { - //File too big return false; } } - //Couldn't tell what file was, local or URL. - return false; - } - - /** - * Get a decoded JSON response from Whatsapp server - * - * @param string $host The host URL - * @param array $query A associative array of keys and values to send to server. - * @return object NULL is returned if the json cannot be decoded or if the encoded data is deeper than the recursion limit - */ - protected function getResponse($host, $query) - { - // Build the url. - $url = $host . '?'; - foreach ($query as $key => $value) { - $url .= $key . '=' . $value . '&'; - } - $url = rtrim($url, '&'); - - // Open connection. - $ch = curl_init(); - // Configure the connection. - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_USERAGENT, static::WHATSAPP_USER_AGENT); - curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: text/json')); - // This makes CURL accept any peer! - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - - // Get the response. - $response = curl_exec($ch); - - // Close the connection. - curl_close($ch); - - return json_decode($response); + return false; } /** * Process the challenge. * - * - * @param ProtocolNode $node - * The node that contains the challenge. + * @param ProtocolNode $node The node that contains the challenge. */ protected function processChallenge($node) { @@ -1921,15 +2070,32 @@ protected function processChallenge($node) /** * Process inbound data. * - * @param string $data - * The data to process. + * @param $data + * + * @throws Exception */ - protected function processInboundData($data, $autoReceipt = true) + protected function processInboundData($data) { $node = $this->reader->nextTree($data); - if( $node != null ) { - $this->processInboundDataNode($node, $autoReceipt); + if ($node != null) { + $this->processInboundDataNode($node); + } + } + + public function addPendingNode(ProtocolNode $node) + { + $from = $node->getAttribute('from'); + if (strpos($from, Constants::WHATSAPP_SERVER) !== false) { + $number = ExtractNumber($node->getAttribute('from')); + } else { + $number = ExtractNumber($node->getAttribute('participant')); } + + if (!isset($this->pending_nodes[$number])) { + $this->pending_nodes[$number] = []; + } + + $this->pending_nodes[$number][] = $node; } /** @@ -1937,601 +2103,355 @@ protected function processInboundData($data, $autoReceipt = true) * * This also provides a convenient method to use to unit test the event framework. * + * @param ProtocolNode $node + * @param $type + * + * @throws Exception */ - protected function processInboundDataNode(ProtocolNode $node, $autoReceipt = true) { - $this->debugPrint($node->nodeString("rx ") . "\n"); + protected function processInboundDataNode(ProtocolNode $node) + { + $this->timeout = time(); + //echo niceVarDump($node); + $this->debugPrint($node->nodeString('rx ')."\n"); $this->serverReceivedId = $node->getAttribute('id'); - if ($node->getTag() == "challenge") { + if ($node->getTag() == 'challenge') { $this->processChallenge($node); - } - elseif($node->getTag() == "failure" ) - { - - $this->loginStatus = static::DISCONNECTED_STATUS; - - } - elseif ($node->getTag() == "success") { - $this->loginStatus = static::CONNECTED_STATUS; - $challengeData = $node->getData(); - file_put_contents($this->challengeFilename, $challengeData); - $this->writer->setKey($this->outputKey); - } elseif($node->getTag() == "failure") - { - $this->eventManager()->fireLoginFailed( - $this->phoneNumber, - $node->getChild(0)->getTag() - ); - } - elseif($node->getTag() == '' && $node->getAttribute("class") == "message") - { - $this->eventManager()->fireMessageReceivedServer( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('class') - ); - } - elseif($node->getTag() == 'receipt') - { - $this->eventManager()->fireMessageReceivedClient( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('class'), - $node->getAttribute('t') - ); - } - if ($node->getTag() == "message") { - array_push($this->messageQueue, $node); - - if ($node->hasChild('x') && $this->lastId == $node->getAttribute('id')) { - $this->sendNextMessage(); - } - if ($this->newMsgBind && $node->getChild('body')) { - $this->newMsgBind->process($node); + } elseif ($node->getTag() == 'failure') { + $this->loginStatus = Constants::DISCONNECTED_STATUS; + $this->eventManager()->fire('onLoginFailed', + [ + $this->phoneNumber, + $node->getChild(0)->getTag(), + ]); + if ($node->getChild(0)->getTag() == 'not-authorized') { + $this->logFile('error', 'Blocked number or wrong password. Use blockChecker.php'); } - if ($node->getAttribute("type") == "text" && $node->getChild('body') != null) { - $author = $node->getAttribute("participant"); - if($author == "") - { - //private chat message - $this->eventManager()->fireGetMessage( + } elseif ($node->getTag() == 'success') { + if ($node->getAttribute('status') == 'active') { + $this->loginStatus = Constants::CONNECTED_STATUS; + $challengeData = $node->getData(); + file_put_contents($this->challengeFilename, $challengeData); + $this->writer->setKey($this->outputKey); + + $this->eventManager()->fire('onLoginSuccess', + [ $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('type'), - $node->getAttribute('t'), - $node->getAttribute("notify"), - $node->getChild("body")->getData() - ); - } - else - { - //group chat message - $this->eventManager()->fireGetGroupMessage( + $node->getAttribute('kind'), + $node->getAttribute('status'), + $node->getAttribute('creation'), + $node->getAttribute('expiration'), + ]); + } elseif ($node->getAttribute('status') == 'expired') { + $this->eventManager()->fire('onAccountExpired', + [ $this->phoneNumber, - $node->getAttribute('from'), - $author, - $node->getAttribute('id'), - $node->getAttribute('type'), - $node->getAttribute('t'), - $node->getAttribute("notify"), - $node->getChild("body")->getData() - ); - } - - if($autoReceipt) - { - $this->sendMessageReceived($node); - } + $node->getAttribute('kind'), + $node->getAttribute('status'), + $node->getAttribute('creation'), + $node->getAttribute('expiration'), + ]); } - if ($node->getAttribute("type") == "media" && $node->getChild('media') != null) { - if ($node->getChild("media")->getAttribute('type') == 'image') { - $this->eventManager()->fireGetImage( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('type'), - $node->getAttribute('t'), - $node->getAttribute('notify'), - $node->getChild("media")->getAttribute('size'), - $node->getChild("media")->getAttribute('url'), - $node->getChild("media")->getAttribute('file'), - $node->getChild("media")->getAttribute('mimetype'), - $node->getChild("media")->getAttribute('filehash'), - $node->getChild("media")->getAttribute('width'), - $node->getChild("media")->getAttribute('height'), - $node->getChild("media")->getData() - ); - } elseif ($node->getChild("media")->getAttribute('type') == 'video') { - $this->eventManager()->fireGetVideo( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('type'), - $node->getAttribute('t'), - $node->getAttribute('notify'), - $node->getChild("media")->getAttribute('url'), - $node->getChild("media")->getAttribute('file'), - $node->getChild("media")->getAttribute('size'), - $node->getChild("media")->getAttribute('mimetype'), - $node->getChild("media")->getAttribute('filehash'), - $node->getChild("media")->getAttribute('duration'), - $node->getChild("media")->getAttribute('vcodec'), - $node->getChild("media")->getAttribute('acodec'), - $node->getChild("media")->getData() - ); - } elseif ($node->getChild("media")->getAttribute('type') == 'audio') { - $this->eventManager()->fireGetAudio( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('type'), - $node->getAttribute('t'), - $node->getAttribute('notify'), - $node->getChild("media")->getAttribute('size'), - $node->getChild("media")->getAttribute('url'), - $node->getChild("media")->getAttribute('file'), - $node->getChild("media")->getAttribute('mimetype'), - $node->getChild("media")->getAttribute('filehash'), - $node->getChild("media")->getAttribute('duration'), - $node->getChild("media")->getAttribute('acodec') - ); - } elseif ($node->getChild("media")->getAttribute('type') == 'vcard') { - $this->eventManager()->fireGetvCard( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('type'), - $node->getAttribute('t'), - $node->getAttribute('notify'), - $node->getChild("media")->getChild("vcard")->getAttribute('name'), - $node->getChild("media")->getChild("vcard")->getData() - ); - } elseif ($node->getChild("media")->getAttribute('type') == 'location') { - $url = $node->getChild("media")->getAttribute('url'); - $name = $node->getChild("media")->getAttribute('name'); - - $this->eventManager()->fireGetLocation( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('type'), - $node->getAttribute('t'), - $node->getAttribute('notify'), - $name, - $node->getChild("media")->getAttribute('longitude'), - $node->getChild("media")->getAttribute('latitude'), - $url, - $node->getChild("media")->getData() - ); - } - - if($autoReceipt) - { - $this->sendMessageReceived($node); + } elseif ($node->getTag() == 'ack') { + if ($node->getAttribute('class') == 'message') { + $this->eventManager()->fire('onMessageReceivedServer', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $node->getAttribute('id'), + $node->getAttribute('class'), + $node->getAttribute('t'), + ]); + } + } elseif ($node->getTag() == 'receipt') { + if ($node->hasChild('list')) { + foreach ($node->getChild('list')->getChildren() as $child) { + $this->eventManager()->fire('onMessageReceivedClient', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $child->getAttribute('id'), + $node->getAttribute('type'), + $node->getAttribute('t'), + $node->getAttribute('participant'), + ]); } } - if ($node->getChild('received') != null) { - $this->eventManager()->fireMessageReceivedClient( + if ($node->hasChild('retry')) { + $this->sendGetCipherKeysFromUser(ExtractNumber($node->getAttribute('from')), true); + $this->messageStore->setPending($node->getAttribute('id'), $node->getAttribute('from')); + } + if ($node->hasChild('error') && $node->getChild('error')->getAttribute('type') == 'enc-v1') { + $this->v1Only[ExtractNumber($node->getAttribute('from'))] = true; + $this->messageStore->setPending($node->getAttribute('id'), $node->getAttribute('from')); + $this->sendPendingMessages($node->getAttribute('from')); + } + + $this->eventManager()->fire('onMessageReceivedClient', + [ $this->phoneNumber, $node->getAttribute('from'), $node->getAttribute('id'), $node->getAttribute('type'), - $node->getAttribute('t') - ); - } + $node->getAttribute('t'), + $node->getAttribute('participant'), + ]); + + $this->sendAck($node, 'receipt'); + } + if ($node->getTag() == 'message') { + $handler = new MessageHandler($this, $node); } - if ($node->getTag() == "presence" && $node->getAttribute("status") == "dirty") { + if ($node->getTag() == 'presence' && $node->getAttribute('status') == 'dirty') { //clear dirty - $categories = array(); - if (count($node->getChildren()) > 0) + $categories = []; + if (count($node->getChildren()) > 0) { foreach ($node->getChildren() as $child) { - if ($child->getTag() == "category") { - $categories[] = $child->getAttribute("name"); + if ($child->getTag() == 'category') { + $categories[] = $child->getAttribute('name'); } } + } $this->sendClearDirty($categories); } - if (strcmp($node->getTag(), "presence") == 0 + if (strcmp($node->getTag(), 'presence') == 0 && strncmp($node->getAttribute('from'), $this->phoneNumber, strlen($this->phoneNumber)) != 0 - && strpos($node->getAttribute('from'), "-") == false) { - $presence = array(); - if($node->getAttribute('type') == null){ - $this->eventManager()->firePresence( - $this->phoneNumber, - $node->getAttribute('from'), - $presence['type'] = "available" - ); - } - else{ - $this->eventManager()->firePresence( - $this->phoneNumber, - $node->getAttribute('from'), - $presence['type'] = "unavailable" - ); + && strpos($node->getAttribute('from'), '-') === false) { + $presence = []; + if ($node->getAttribute('type') == null) { + $this->eventManager()->fire('onPresenceAvailable', + [ + $this->phoneNumber, + $node->getAttribute('from'), + ]); + } else { + $this->eventManager()->fire('onPresenceUnavailable', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $node->getAttribute('last'), + ]); } } - if ($node->getTag() == "presence" + if ($node->getTag() == 'presence' && strncmp($node->getAttribute('from'), $this->phoneNumber, strlen($this->phoneNumber)) != 0 - && strpos($node->getAttribute('from'), "-") !== false + && strpos($node->getAttribute('from'), '-') !== false && $node->getAttribute('type') != null) { - $groupId = self::parseJID($node->getAttribute('from')); + $groupId = $this->parseJID($node->getAttribute('from')); if ($node->getAttribute('add') != null) { - $this->eventManager()->fireGroupsParticipantsAdd( - $this->phoneNumber, - $groupId, - self::parseJID($node->getAttribute('add')) - ); + $this->eventManager()->fire('onGroupsParticipantsAdd', + [ + $this->phoneNumber, + $groupId, + $this->parseJID($node->getAttribute('add')), + ]); } elseif ($node->getAttribute('remove') != null) { - $this->eventManager()->fireGroupsParticipantsRemove( - $this->phoneNumber, - $groupId, - self::parseJID($node->getAttribute('remove')), - self::parseJID($node->getAttribute('author')) - ); - } - } - if (strcmp($node->getTag(), "chatstate") == 0 - && strncmp($node->getAttribute('from'), $this->phoneNumber, strlen($this->phoneNumber)) != 0 - && strpos($node->getAttribute('from'), "-") == false) { - if($node->getChild(0)->getTag() == "composing"){ - $this->eventManager()->fireMessageComposing( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - "composing", - $node->getAttribute('t') - ); - } - else{ - $this->eventManager()->fireMessagePaused( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - "paused", - $node->getAttribute('t') - ); - } - } - if ($node->getTag() == "iq" - && $node->getAttribute('type') == "get" - && $node->getAttribute('xmlns') == "urn:xmpp:ping") { - $this->eventManager()->firePing( - $this->phoneNumber, - $node->getAttribute('id') - ); - $this->sendPong($node->getAttribute('id')); - } - if ($node->getTag() == "iq" - && $node->getChild("sync") != null) { - - //sync result - $sync = $node->getChild('sync'); - $existing = $sync->getChild("in"); - $nonexisting = $sync->getChild("out"); - - //process existing first - $existingUsers = array(); - if (!empty($existing)) { - foreach ($existing->getChildren() as $child) { - $existingUsers[$child->getData()] = $child->getAttribute("jid"); - } - } - - //now process failed numbers - $failedNumbers = array(); - if (!empty($nonexisting)) { - foreach ($nonexisting->getChildren() as $child) { - $failedNumbers[] = str_replace('+', '', $child->getData()); - } + $this->eventManager()->fire('onGroupsParticipantsRemove', + [ + $this->phoneNumber, + $groupId, + $this->parseJID($node->getAttribute('remove')), + ]); } - - $index = $sync->getAttribute("index"); - - $result = new SyncResult($index, $sync->getAttribute("sync"), $existingUsers, $failedNumbers); - - $this->eventManager()->fireGetSyncResult($result); - } - if ($node->getTag() == "receipt") { - $this->eventManager()->fireGetReceipt( - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('offline'), - $node->getAttribute('retry') - ); } - if ($node->getTag() == "iq" - && $node->getAttribute('type') == "result") { - if ($node->getChild("query") != null) { - if ($node->getChild(0)->getAttribute('xmlns') == 'jabber:iq:privacy') { - // ToDo: We need to get explicitly list out the children as arguments - // here. - $this->eventManager()->fireGetPrivacyBlockedList( - $this->phoneNumber, - $node->getChild(0)->getChild(0)->getChildren() - ); - } - $this->eventManager()->fireGetRequestLastSeen( + if (strcmp($node->getTag(), 'chatstate') == 0 + && strncmp($node->getAttribute('from'), $this->phoneNumber, strlen($this->phoneNumber)) != 0) { // remove if isn't group + if (strpos($node->getAttribute('from'), '-') === false) { + if ($node->getChild(0)->getTag() == 'composing') { + $this->eventManager()->fire('onMessageComposing', + [ $this->phoneNumber, $node->getAttribute('from'), $node->getAttribute('id'), - $node->getChild(0)->getAttribute('seconds') - ); - array_push($this->messageQueue, $node); - } - if ($node->getChild("props") != null) { - //server properties - $props = array(); - foreach($node->getChild(0)->getChildren() as $child) { - $props[$child->getAttribute("name")] = $child->getAttribute("value"); - } - $this->eventManager()->fireGetServerProperties( - $this->phoneNumber, - $node->getChild(0)->getAttribute("version"), - $props - ); - } - if ($node->getChild("picture") != null) { - $this->eventManager()->fireGetProfilePicture( + 'composing', + $node->getAttribute('t'), + ]); + } else { + $this->eventManager()->fire('onMessagePaused', + [ $this->phoneNumber, - $node->getAttribute("from"), - $node->getChild("picture")->getAttribute("type"), - $node->getChild("picture")->getData() - ); - } - if ($node->getChild("media") != null || $node->getChild("duplicate") != null) { - $this->processUploadResponse($node); - } - if ($node->nodeIdContains("group")) { - //There are multiple types of Group reponses. Also a valid group response can have NO children. - //Events fired depend on text in the ID field. - $groupList = array(); - if ($node->getChild(0) != null) { - foreach ($node->getChildren() as $child) { - $groupList[] = $child->getAttributes(); - } - } - if($node->nodeIdContains('creategroup')){ - $this->groupId = $node->getChild(0)->getAttribute('id'); - $this->eventManager()->fireGroupsChatCreate( - $this->phoneNumber, - $this->groupId - ); - } - if($node->nodeIdContains('endgroup')){ - $this->groupId = $node->getChild(0)->getChild(0)->getAttribute('id'); - $this->eventManager()->fireGroupsChatEnd( - $this->phoneNumber, - $this->groupId - ); - } - if($node->nodeIdContains('getgroups')){ - $this->eventManager()->fireGetGroups( - $this->phoneNumber, - $groupList - ); - } - if($node->nodeIdContains('getgroupinfo')){ - $this->eventManager()->fireGetGroupsInfo( - $this->phoneNumber, - $groupList - ); - } - if($node->nodeIdContains('getgroupparticipants')){ - $groupId = self::parseJID($node->getAttribute('from')); - $this->eventManager()->fireGetGroupParticipants( - $this->phoneNumber, - $groupId, - $groupList - ); + $node->getAttribute('from'), + $node->getAttribute('id'), + 'paused', + $node->getAttribute('t'), + ]); } - } - if($node->getChild("status") != null) - { - $child = $node->getChild("status"); - foreach($child->getChildren() as $status) - { - $this->eventManager()->fireGetStatus( - $this->phoneNumber, - $status->getAttribute("jid"), - "requested", - $node->getAttribute("id"), - $status->getAttribute("t"), - $status->getData() - ); + } else { + if ($node->getChild(0)->getTag() == 'composing') { + $this->eventManager()->fire('onGroupMessageComposing', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $node->getAttribute('participant'), + $node->getAttribute('id'), + 'composing', + $node->getAttribute('t'), + ]); + } else { + $this->eventManager()->fire('onGroupMessagePaused', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $node->getAttribute('participant'), + $node->getAttribute('id'), + 'paused', + $node->getAttribute('t'), + ]); } } } - if ($node->getTag() == "iq" && $node->getAttribute('type') == "error") { - $this->eventManager()->fireGetError( - $this->phoneNumber, - $node->getAttribute( 'id' ), - $node->getChild(0) - ); + if ($node->getTag() == 'receipt') { + $this->eventManager()->fire('onGetReceipt', + [ + $node->getAttribute('from'), + $node->getAttribute('id'), + $node->getAttribute('offline'), + $node->getAttribute('retry'), + ]); + } + if ($node->getTag() == 'iq') { + $handler = new IqHandler($this, $node); } - $children = $node->getChild(0); - if ($node->getTag() == "stream:error" && empty($children) == false && $node->getChild(0)->getTag() == "system-shutdown") - { - - throw new Exception('Error system-shutdown'); - + if ($node->getTag() == 'notification') { + $handler = new NotificationHandler($this, $node); } + if ($node->getTag() == 'call') { + if ($node->getChild(0)->getTag() == 'offer') { + $callId = $node->getChild(0)->getAttribute('call-id'); + $this->sendReceipt($node, null, null, $callId); - if($node->getTag() == "notification") - { - $name = $node->getAttribute("notify"); - $type = $node->getAttribute("type"); - switch($type) - { - case "status": - $this->eventManager()->fireGetStatus( - $this->phoneNumber, //my number - $node->getAttribute("from"), //sender - $node->getChild(0)->getTag(), //type (set/remove) - $node->getAttribute("id"), //message id - $node->getAttribute("t"), //time - $node->getChild(0)->getData()); //status message - break; - case "picture": - if ($node->hasChild('set')) { - $this->eventManager()->fireProfilePictureChanged( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('t') - ); - } else if ($node->hasChild('delete')) { - $this->eventManager()->fireProfilePictureDeleted( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('id'), - $node->getAttribute('t') - ); - } - //TODO - break; - case "contacts": - //TODO - break; - case "participant": - if ($node->hasChild('remove')) { - $this->eventManager()->fireGroupsParticipantsRemove( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getChild(0)->getAttribute('jid'), - $node->getChild(0)->getAttribute('author') - ); - } else if ($node->hasChild('add')) { - $this->eventManager()->fireGroupsParticipantsAdd( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getChild(0)->getAttribute('jid') - ); - } - //TODO - break; - case "subject": - $this->eventManager()->fireGetGroupsSubject( - $this->phoneNumber, - $node->getAttribute('from'), - $node->getAttribute('t'), - $node->getAttribute('participant'), - $node->getAttribute('participant'), - $node->getAttribute('notify'), - $node->getChild(0)->getData() - ); - //TODO - break; - default: - throw new Exception("Method $type not implemented"); + $this->eventManager()->fire('onCallReceived', + [ + $this->phoneNumber, + $node->getAttribute('from'), + $node->getAttribute('id'), + $node->getAttribute('notify'), + $node->getAttribute('t'), + $node->getChild(0)->getAttribute('call-id'), + ]); + } else { + $this->sendAck($node, 'call'); } - $this->sendNotificationAck($node); } - if($node->getTag() == "ib") - { - foreach($node->getChildren() as $child) - { - switch($child->getTag()) - { - case "dirty": - $this->sendClearDirty(array($child->getAttribute("type"))); + if ($node->getTag() == 'ib') { + foreach ($node->getChildren() as $child) { + switch ($child->getTag()) { + case 'dirty': + $this->sendClearDirty([$child->getAttribute('type')]); break; - case "offline": + case 'account': + $this->eventManager()->fire('onPaymentRecieved', + [ + $this->phoneNumber, + $child->getAttribute('kind'), + $child->getAttribute('status'), + $child->getAttribute('creation'), + $child->getAttribute('expiration'), + ]); + break; + case 'offline': break; default: - throw new Exception("ib handler for " . $child->getTag() . " not implemented"); + throw new Exception('ib handler for '.$child->getTag().' not implemented'); } } } - if($node->getTag() == "ack") - { - ////on get ack - } + // Disconnect socket on stream error. + if ($node->getTag() == 'stream:error') { + $this->eventManager()->fire('onStreamError', + [ + $node->getChild(0)->getTag(), + ]); + + $this->logFile('error', 'Stream error {error}', ['error' => $node->getChild(0)->getTag()]); + $this->disconnect(); + } + if (isset($handler)) { + $handler->Process(); + unset($handler); + } } /** - * @param $node ProtocolNode + * @param $node ProtocolNode + * @param $class string */ - protected function sendNotificationAck($node) + public function sendAck($node, $class, $isGroup = false) { - $from = $node->getAttribute("from"); - $to = $node->getAttribute("to"); - $participant = $node->getAttribute("participant"); - $id = $node->getAttribute("id"); - $type = $node->getAttribute("type"); + $from = $node->getAttribute('from'); + $to = $node->getAttribute('to'); + $id = $node->getAttribute('id'); + $participant = null; + $type = null; + if (!$isGroup) { + $type = $node->getAttribute('type'); + $participant = $node->getAttribute('participant'); + } + + $attributes = []; + if ($to) { + $attributes['from'] = $to; + } + if ($participant) { + $attributes['participant'] = $participant; + } + if ($isGroup) { + $attributes['count'] = $this->retryCounters[$id]; + } + $attributes['to'] = $from; + $attributes['class'] = $class; + $attributes['id'] = $id; + // if ($node->getAttribute("id") != null) + // $attributes["t"] = $node->getAttribute("t"); + if ($type != null) { + $attributes['type'] = $type; + } + + $ack = new ProtocolNode('ack', $attributes, null, null); - $attributes = array(); - if($to) - $attributes["from"] = $to; - if($participant) - $attributes["participant"] = $participant; - $attributes["to"] = $from; - $attributes["class"] = "notification"; - $attributes["id"] = $id; - $attributes["type"] = $type; - $ack = new ProtocolNode("ack", $attributes, null, null); $this->sendNode($ack); } /** - * Process and save media image + * Process and save media image. * - * @param ProtocolNode $node - * ProtocolNode containing media + * @param ProtocolNode $node ProtocolNode containing media */ protected function processMediaImage($node) { - $media = $node->getChild("media"); + $media = $node->getChild('media'); + if ($media != null) { - $filename = $media->getAttribute("file"); - $url = $media->getAttribute("url"); + $filename = $media->getAttribute('file'); + $url = $media->getAttribute('url'); //save thumbnail - $data = $media->getData(); - $fp = @fopen(static::MEDIA_FOLDER . "/thumb_" . $filename, "w"); - if ($fp) { - fwrite($fp, $data); - fclose($fp); - } - + file_put_contents($this->dataFolder.Constants::MEDIA_FOLDER.DIRECTORY_SEPARATOR.'thumb_'.$filename, $media->getData()); //download and save original - $data = file_get_contents($url); - $fp = @fopen(static::MEDIA_FOLDER . "/" . $filename, "w"); - if ($fp) { - fwrite($fp, $data); - fclose($fp); - } + file_put_contents($this->dataFolder.Constants::MEDIA_FOLDER.DIRECTORY_SEPARATOR.$filename, file_get_contents($url)); } } /** - * Processes received picture node + * Processes received picture node. * - * @param ProtocolNode $node - * ProtocolNode containing the picture + * @param ProtocolNode $node ProtocolNode containing the picture */ protected function processProfilePicture($node) { - $pictureNode = $node->getChild("picture"); + $pictureNode = $node->getChild('picture'); if ($pictureNode != null) { - $type = $pictureNode->getAttribute("type"); - $data = $pictureNode->getData(); - if ($type == "preview") { - $filename = static::PICTURES_FOLDER . "/preview_" . $node->getAttribute("from") . ".jpg"; + if ($pictureNode->getAttribute('type') == 'preview') { + $filename = $this->dataFolder.Constants::PICTURES_FOLDER.DIRECTORY_SEPARATOR.'preview_'.$node->getAttribute('from').'jpg'; } else { - $filename = static::PICTURES_FOLDER . "/" . $node->getAttribute("from") . ".jpg"; - } - $fp = @fopen($filename, "w"); - if ($fp) { - fwrite($fp, $data); - fclose($fp); + $filename = $this->dataFolder.Constants::PICTURES_FOLDER.DIRECTORY_SEPARATOR.$node->getAttribute('from').'.jpg'; } + + file_put_contents($filename, $pictureNode->getData()); } } @@ -2543,53 +2463,53 @@ protected function processProfilePicture($node) */ protected function processTempMediaFile($storeURLmedia) { - if (isset($this->mediaFileInfo['url'])) { - if ($storeURLmedia) { - if (is_file($this->mediaFileInfo['filepath'])) { - rename($this->mediaFileInfo['filepath'], $this->mediaFileInfo['filepath'] . $this->mediaFileInfo['fileextension']); - } - } else { - if (is_file($this->mediaFileInfo['filepath'])) { - unlink($this->mediaFileInfo['filepath']); - } - } + if (!isset($this->mediaFileInfo['url'])) { + return false; + } + + if ($storeURLmedia && is_file($this->mediaFileInfo['filepath'])) { + rename($this->mediaFileInfo['filepath'], $this->mediaFileInfo['filepath'].'.'.$this->mediaFileInfo['fileextension']); + } elseif (is_file($this->mediaFileInfo['filepath'])) { + unlink($this->mediaFileInfo['filepath']); } } /** - * Process media upload response + * Process media upload response. + * + * @param ProtocolNode $node Message node * - * @param ProtocolNode $node - * Message node * @return bool */ - protected function processUploadResponse($node) + public function processUploadResponse($node) { - $id = $node->getAttribute("id"); + $id = $node->getAttribute('id'); $messageNode = @$this->mediaQueue[$id]; if ($messageNode == null) { //message not found, can't send! - $this->eventManager()->fireMediaUploadFailed( - $this->phoneNumber, - $id, - $node, - $messageNode, - "Message node not found in queue" - ); + $this->eventManager()->fire('onMediaUploadFailed', + [ + $this->phoneNumber, + $id, + $node, + $messageNode, + 'Message node not found in queue', + ]); + return false; } - $duplicate = $node->getChild("duplicate"); + $duplicate = $node->getChild('duplicate'); if ($duplicate != null) { //file already on whatsapp servers - $url = $duplicate->getAttribute("url"); - $filesize = $duplicate->getAttribute("size"); -// $mimetype = $duplicate->getAttribute("mimetype"); - $filehash = $duplicate->getAttribute("filehash"); - $filetype = $duplicate->getAttribute("type"); -// $width = $duplicate->getAttribute("width"); -// $height = $duplicate->getAttribute("height"); - $exploded = explode("/", $url); + $url = $duplicate->getAttribute('url'); + $filesize = $duplicate->getAttribute('size'); +// $mimetype = $duplicate->getAttribute("mimetype"); + $filehash = $duplicate->getAttribute('filehash'); + $filetype = $duplicate->getAttribute('type'); +// $width = $duplicate->getAttribute("width"); +// $height = $duplicate->getAttribute("height"); + $exploded = explode('/', $url); $filename = array_pop($exploded); } else { //upload new file @@ -2597,102 +2517,126 @@ protected function processUploadResponse($node) if (!$json) { //failed upload - $this->eventManager()->fireMediaUploadFailed( - $this->phoneNumber, - $id, - $node, - $messageNode, - "Failed to push file to server" - ); + $this->eventManager()->fire('onMediaUploadFailed', + [ + $this->phoneNumber, + $id, + $node, + $messageNode, + 'Failed to push file to server', + ]); + return false; } $url = $json->url; $filesize = $json->size; -// $mimetype = $json->mimetype; +// $mimetype = $json->mimetype; $filehash = $json->filehash; $filetype = $json->type; -// $width = $json->width; -// $height = $json->height; +// $width = $json->width; +// $height = $json->height; $filename = $json->name; } - $mediaAttribs = array(); - $mediaAttribs["xmlns"] = "urn:xmpp:whatsapp:mms"; - $mediaAttribs["type"] = $filetype; - $mediaAttribs["url"] = $url; - $mediaAttribs["file"] = $filename; - $mediaAttribs["size"] = $filesize; - $mediaAttribs["hash"] = $filehash; + $mediaAttribs = []; + $mediaAttribs['type'] = $filetype; + $mediaAttribs['url'] = $url; + $mediaAttribs['encoding'] = 'raw'; + $mediaAttribs['file'] = $filename; + $mediaAttribs['size'] = $filesize; + if ($this->mediaQueue[$id]['caption'] != '') { + $mediaAttribs['caption'] = $this->mediaQueue[$id]['caption']; + } + if ($this->voice == true) { + $mediaAttribs['origin'] = 'live'; + $this->voice = false; + } $filepath = $this->mediaQueue[$id]['filePath']; $to = $this->mediaQueue[$id]['to']; switch ($filetype) { - case "image": + case 'image': + $caption = $this->mediaQueue[$id]['caption']; $icon = createIcon($filepath); break; - case "video": + case 'video': + $caption = $this->mediaQueue[$id]['caption']; $icon = createVideoIcon($filepath); break; default: + $caption = ''; $icon = ''; break; } + //Retrieve Message ID + $message_id = $messageNode['message_id']; - $mediaNode = new ProtocolNode("media", $mediaAttribs, null, $icon); + $mediaNode = new ProtocolNode('media', $mediaAttribs, null, $icon); if (is_array($to)) { - $this->sendBroadcast($to, $mediaNode, "media"); + $this->sendBroadcast($to, $mediaNode, 'media'); } else { - $this->sendMessageNode($to, $mediaNode); - } - $this->eventManager()->fireMediaMessageSent( - $this->phoneNumber, - $to, - $id, - $filetype, - $url, - $filename, - $filesize, - $filehash, - $icon - ); + $this->sendMessageNode($to, $mediaNode, $message_id); + } + $this->eventManager()->fire('onMediaMessageSent', + [ + $this->phoneNumber, + $to, + $message_id, + $filetype, + $url, + $filename, + $filesize, + $filehash, + $caption, + $icon, + ]); + return true; } /** * Read 1024 bytes from the whatsapp server. + * + * @throws Exception */ public function readStanza() { $buff = ''; - if($this->socket != null) - { - $header = @fread($this->socket, 3);//read stanza header - if(strlen($header) == 0) - { + + if ($this->isConnected()) { + $header = @socket_read($this->socket, 3); //read stanza header + // if($header !== false && strlen($header) > 1){ + + if ($header === false) { + $this->eventManager()->fire('onClose', + [ + $this->phoneNumber, + 'Socket EOF', + ] + ); + } + if (strlen($header) == 0) { //no data received return; } - if(strlen($header) != 3) - { - throw new Exception("Failed to read stanza header"); + if (strlen($header) != 3) { + throw new ConnectionException('Failed to read stanza header'); } - $treeLength = 0; - $treeLength = ord($header[1]) << 8; + $treeLength = (ord($header[0]) & 0x0F) << 16; + $treeLength |= ord($header[1]) << 8; $treeLength |= ord($header[2]) << 0; //read full length - $buff = @fread($this->socket, $treeLength); - $trlen = $treeLength; + $buff = socket_read($this->socket, $treeLength); + //$trlen = $treeLength; $len = strlen($buff); - $prev = 0; - while(strlen($buff) < $treeLength) - { + //$prev = 0; + while (strlen($buff) < $treeLength) { $toRead = $treeLength - strlen($buff); - $buff .= @fread($this->socket, $toRead); - if($len == strlen($buff)) - { + $buff .= socket_read($this->socket, $toRead); + if ($len == strlen($buff)) { //no new data read, fuck it break; } @@ -2700,22 +2644,9 @@ public function readStanza() } if (strlen($buff) != $treeLength) { - throw new Exception("Tree length did not match received length (buff = " . strlen($buff) . " & treeLength = $treeLength)"); - } else - if (@feof($this->socket)) { - $error = "socket EOF, closing socket..."; - fclose($this->socket); - $this->socket = null; - $this->eventManager()->fireClose( - $this->phoneNumber, - $error - ); + throw new ConnectionException('Tree length did not match received length (buff = '.strlen($buff)." & treeLength = $treeLength)"); } - $buff = $header . $buff; - } - else - { - throw new Exception("Socket closed"); + $buff = $header.$buff; } return $buff; @@ -2724,369 +2655,520 @@ public function readStanza() /** * Checks that the media file to send is of allowable filetype and within size limits. * - * @param string $filepath The URL/URI to the media file - * @param int $maxSize Maximim filesize allowed for media type - * @param string $to Recipient ID/number - * @param string $type media filetype. 'audio', 'video', 'image' - * @param array $allowedExtensions An array of allowable file types for the media file - * @param bool $storeURLmedia Keep a copy of the media file - * @return bool + * @param string $filepath The URL/URI to the media file + * @param int $maxSize Maximum filesize allowed for media type + * @param string $to Recipient ID/number + * @param string $type media filetype. 'audio', 'video', 'image' + * @param array $allowedExtensions An array of allowable file types for the media file + * @param bool $storeURLmedia Keep a copy of the media file + * @param string $caption * + * + * @return string|null Message ID if successfully, null if not. */ - protected function sendCheckAndSendMedia($filepath, $maxSize, $to, $type, $allowedExtensions, $storeURLmedia) + protected function sendCheckAndSendMedia($filepath, $maxSize, $to, $type, $allowedExtensions, $storeURLmedia, $caption = '') { if ($this->getMediaFile($filepath, $maxSize) == true) { - if (in_array($this->mediaFileInfo['fileextension'], $allowedExtensions)) { - $b64hash = base64_encode(hash_file("sha256", $this->mediaFileInfo['filepath'], true)); - //request upload - $this->sendRequestFileUpload($b64hash, $type, $this->mediaFileInfo['filesize'], $this->mediaFileInfo['filepath'], $to); + if (in_array(strtolower($this->mediaFileInfo['fileextension']), $allowedExtensions)) { + $b64hash = base64_encode(hash_file('sha256', $this->mediaFileInfo['filepath'], true)); + //request upload and get Message ID + $id = $this->sendRequestFileUpload($b64hash, $type, $this->mediaFileInfo['filesize'], $this->mediaFileInfo['filepath'], $to, $caption); $this->processTempMediaFile($storeURLmedia); - return true; + // Return message ID. Make pull request for this. + return $id; } else { //Not allowed file type. $this->processTempMediaFile($storeURLmedia); - return false; + + return; } } else { //Didn't get media file details. - return false; + return; } } /** - * Send a broadcast - * @param array $targets Array of numbers to send to - * @param object $node + * Send a broadcast. + * + * @param array $targets Array of numbers to send to + * @param object $node + * @param $type + * + * @return string */ protected function sendBroadcast($targets, $node, $type) { if (!is_array($targets)) { - $targets = array($targets); + $targets = [$targets]; } - $serverNode = new ProtocolNode("server", null, null, ""); - $xHash = array(); - $xHash["xmlns"] = "jabber:x:event"; - $xNode = new ProtocolNode("x", $xHash, array($serverNode), ""); - - $toNodes = array(); + $toNodes = []; foreach ($targets as $target) { $jid = $this->getJID($target); - $hash = array("jid" => $jid); - $toNode = new ProtocolNode("to", $hash, null, null); + $hash = ['jid' => $jid]; + $toNode = new ProtocolNode('to', $hash, null, null); $toNodes[] = $toNode; } - $broadcastNode = new ProtocolNode("broadcast", null, $toNodes, null); + $broadcastNode = new ProtocolNode('broadcast', null, $toNodes, null); + + $msgId = $this->createMsgId(); - $messageHash = array(); - $messageHash["to"] = time()."@broadcast"; - $messageHash["type"] = $type; - $id = $this->createMsgId("broadcast"); - $messageHash["id"] = $id; + $messageNode = new ProtocolNode('message', + [ + 'to' => time().'@broadcast', + 'type' => $type, + 'id' => $msgId, + ], [$node, $broadcastNode], null); - $messageNode = new ProtocolNode("message", $messageHash, array($broadcastNode, $xNode, $node), null); $this->sendNode($messageNode); + $this->waitForServer($msgId); //listen for response - $this->eventManager()->fireSendMessage( - $this->phoneNumber, - $targets, - $messageHash["id"], - $node - ); - return $id; + $this->eventManager()->fire('onSendMessage', + [ + $this->phoneNumber, + $targets, + $msgId, + $node, + ]); + + return $msgId; } /** - * Send data to the whatsapp server. + * Send data to the WhatsApp server. + * * @param string $data + * + * @throws Exception */ - protected function sendData($data) + public function sendData($data) { - if($this->socket != null) - { - fwrite($this->socket, $data, strlen($data)); + if ($this->isConnected()) { + if (socket_write($this->socket, $data, strlen($data)) === false) { + $this->eventManager()->fire('onClose', + [ + $this->phoneNumber, + 'Connection closed!', + ] + ); + } } } /** - * Send the getGroupList request to Whatsapp - * @param string $type Type of list of groups to retrieve. "owning" or "participating" + * Send the getGroupList request to WhatsApp. + * + * @param string $type Type of list of groups to retrieve. "owning" or "participating" */ protected function sendGetGroupsFiltered($type) { - $msgID = $this->createMsgId("getgroups"); - $child = new ProtocolNode("list", array( - "type" => $type - ), null, null); - $node = new ProtocolNode("iq", array( - "id" => $msgID, - "type" => "get", - "xmlns" => "w:g", - "to" => "g.us" - ), array($child), null); + $msgID = $this->nodeId['getgroups'] = $this->createIqId(); + $child = new ProtocolNode($type, null, null, null); + $node = new ProtocolNode('iq', + [ + 'id' => $msgID, + 'type' => 'get', + 'xmlns' => 'w:g2', + 'to' => Constants::WHATSAPP_GROUP_SERVER, + ], [$child], null); + $this->sendNode($node); - $this->waitForServer($msgID); } /** * Change participants of a group. * - * @param string $groupId - * The group ID. - * @param array $participants - * An array with the participants. - * @param string $tag - * The tag action. 'add' or 'remove' + * @param string $groupId The group ID. + * @param string $participant The participant. + * @param string $tag The tag action. 'add', 'remove', 'promote' or 'demote' + * @param $id */ - protected function sendGroupsChangeParticipants($groupId, $participants, $tag) + protected function sendGroupsChangeParticipants($groupId, $participant, $tag, $id) { - $_participants = array(); - foreach ($participants as $participant) { - $_participants[] = new ProtocolNode("participant", array("jid" => $this->getJID($participant)), null, ""); - } + $participants = new ProtocolNode('participant', ['jid' => $this->getJID($participant)], null, ''); - $childHash = array(); - $child = new ProtocolNode($tag, $childHash, $_participants, ""); + $childHash = []; + $child = new ProtocolNode($tag, $childHash, [$participants], ''); - $setHash = array(); - $setHash["id"] = $this->createMsgId("participants"); - $setHash["type"] = "set"; - $setHash["xmlns"] = "w:g"; - $setHash["to"] = $this->getJID($groupId); - - $node = new ProtocolNode("iq", $setHash, array($child), ""); + $node = new ProtocolNode('iq', + [ + 'id' => $id, + 'type' => 'set', + 'xmlns' => 'w:g2', + 'to' => $this->getJID($groupId), + ], [$child], ''); $this->sendNode($node); - $this->waitForServer($setHash["id"]); } /** * Send node to the servers. * - * @param $to - * The recipient to send. - * @param $node ProtocolNode - * The node that contains the message. - */ - protected function sendMessageNode($to, $node, $id = null) - { - $serverNode = new ProtocolNode("server", null, null, ""); - $xHash = array(); - $xHash["xmlns"] = "jabber:x:event"; - $xNode = new ProtocolNode("x", $xHash, array($serverNode), ""); - $notify = array(); - $notify['xmlns'] = 'urn:xmpp:whatsapp'; - $notify['name'] = $this->name; - $notnode = new ProtocolNode("notify", $notify, null, ""); - $request = array(); - $request['xmlns'] = "urn:xmpp:receipts"; - $reqnode = new ProtocolNode("request", $request, null, ""); - - $messageHash = array(); - $messageHash["to"] = $this->getJID($to); - if($node->getTag() == "body") - { - $messageHash["type"] = "text"; - } - else - { - $messageHash["type"] = "media"; - } - $messageHash["id"] = ($id == null?$this->createMsgId("message"):$id); - $messageHash["t"] = time(); - - $messageNode = new ProtocolNode("message", $messageHash, array($xNode, $notnode, $reqnode, $node), ""); + * @param $to + * @param ProtocolNode $node + * @param null $id + * + * @return string Message ID. + */ + protected function sendMessageNode($to, $node, $id = null, $plaintextNode = null) + { + $msgId = ($id == null) ? $this->createMsgId() : $id; + $to = $this->getJID($to); + + if ($node->getTag() == 'body' || $node->getTag() == 'enc') { + $type = 'text'; + } else { + $type = 'media'; + } + + $messageNode = new ProtocolNode('message', [ + 'to' => $to, + 'type' => $type, + 'id' => $msgId, + 't' => time(), + 'notify' => $this->name, + ], [$node], ''); + $this->sendNode($messageNode); - $this->eventManager()->fireSendMessage( - $this->phoneNumber, - $this->getJID($to), - $messageHash["id"], - $node - ); - return $messageHash["id"]; + + if ($node->getTag() == 'enc') { + $node = $plaintextNode; + } + + $this->logFile('info', '{type} message with id {id} sent to {to}', ['type' => $type, 'id' => $msgId, 'to' => ExtractNumber($to)]); + $this->eventManager()->fire('onSendMessage', + [ + $this->phoneNumber, + $to, + $msgId, + $node, + ]); + + // $this->waitForServer($msgId); + + return $msgId; } /** * Tell the server we received the message. * - * @param ProtocolNode $msg - * The ProtocolTreeNode that contains the message. + * @param ProtocolNode $node The ProtocolTreeNode that contains the message. + * @param string $type + * @param string $participant + * @param string $callId + */ + public function sendReceipt($node, $type = 'read', $participant = null, $callId = null) + { + $messageHash = []; + if ($type == 'read') { + $messageHash['type'] = $type; + } + if ($participant != null) { + $messageHash['participant'] = $participant; + } + $messageHash['to'] = $node->getAttribute('from'); + $messageHash['id'] = $node->getAttribute('id'); + $messageHash['t'] = $node->getAttribute('t'); + + if ($callId != null) { + $offerNode = new ProtocolNode('offer', ['call-id' => $callId], null, null); + $messageNode = new ProtocolNode('receipt', $messageHash, [$offerNode], null); + } else { + $messageNode = new ProtocolNode('receipt', $messageHash, null, null); + } + $this->sendNode($messageNode); + $this->eventManager()->fire('onSendMessageReceived', + [ + $this->phoneNumber, + $node->getAttribute('id'), + $node->getAttribute('from'), + $type, + ]); + } + + /** + * Send a read receipt to a message. + * + * @param string $to The recipient. + * @param mixed String or Array $id */ - protected function sendMessageReceived($msg, $type = null) + public function sendMessageRead($to, $id) { - if($type) - { - $messageHash["type"] = $type; + $listNode = null; + $idNode = $id; + if (is_array($id) && count($id > 1)) { + $idNode = array_shift($id); + foreach ($id as $itemId) { + $items[] = new ProtocolNode('item', + [ + 'id' => $itemId, + ], null, null); + } + $listNode = new ProtocolNode('list', null, $items, null); } - $messageHash = array(); - $messageHash["to"] = $msg->getAttribute("from"); - $messageHash["id"] = $msg->getAttribute("id"); - $messageNode = new ProtocolNode("receipt", $messageHash, null, null); + + $messageNode = new ProtocolNode('receipt', + [ + 'type' => 'read', + 't' => time(), + 'to' => $this->getJID($to), + 'id' => $idNode, + ], [$listNode], null); + $this->sendNode($messageNode); - $this->eventManager()->fireSendMessageReceived( - $this->phoneNumber, - $msg->getAttribute("id"), - $msg->getAttribute("from"), - $type - ); } /** * Send node to the WhatsApp server. + * * @param ProtocolNode $node + * @param bool $encrypt */ - protected function sendNode($node, $encrypt = true) + public function sendNode($node, $encrypt = true) { - $this->debugPrint($node->nodeString("tx ") . "\n"); + $this->timeout = time(); + $this->debugPrint($node->nodeString('tx ')."\n"); $this->sendData($this->writer->write($node, $encrypt)); } /** - * Send request to upload file + * Send request to upload file. * - * @param $b64hash - * Base64 hash of file - * @param string $type - * File type - * @param $size - * File size - * @param string $filepath - * Path to image file - * @param string $to - * Recipient + * @param string $b64hash A base64 hash of file + * @param string $type File type + * @param string $size File size + * @param string $filepath Path to image file + * @param mixed $to Recipient(s) + * @param string $caption + * + * @return string Message ID */ - protected function sendRequestFileUpload($b64hash, $type, $size, $filepath, $to) + protected function sendRequestFileUpload($b64hash, $type, $size, $filepath, $to, $caption = '') { - $hash = array(); - $hash["hash"] = $b64hash; - $hash["type"] = $type; - $hash["size"] = $size; - $mediaNode = new ProtocolNode("media", $hash, null, null); - - $hash = array(); - $id = $this->createMsgId("upload"); - $hash["id"] = $id; - $hash["to"] = static::WHATSAPP_SERVER; - $hash["type"] = "set"; - $hash["xmlns"] = "w:m"; - $node = new ProtocolNode("iq", $hash, array($mediaNode), null); + $id = $this->createIqId(); if (!is_array($to)) { $to = $this->getJID($to); } + + $mediaNode = new ProtocolNode('media', [ + 'hash' => $b64hash, + 'type' => $type, + 'size' => $size, + ], null, null); + + $node = new ProtocolNode('iq', [ + 'id' => $id, + 'to' => Constants::WHATSAPP_SERVER, + 'type' => 'set', + 'xmlns' => 'w:m', + ], [$mediaNode], null); + //add to queue - $messageId = $this->createMsgId("message"); - $this->mediaQueue[$id] = array("messageNode" => $node, "filePath" => $filepath, "to" => $to, "message_id" => $messageId); + $messageId = $this->createMsgId(); + $this->mediaQueue[$id] = [ + 'messageNode' => $node, + 'filePath' => $filepath, + 'to' => $to, + 'message_id' => $messageId, + 'caption' => $caption, + ]; $this->sendNode($node); - $this->waitForServer($hash["id"]); + $this->waitForServer($id); + + // Return message ID. Make pull request for this. + return $messageId; } /** - * Set your profile picture + * Set your profile picture. * * @param string $jid - * @param string $filepath - * URL or localpath to image file + * @param string $filepath URL or localpath to image file */ protected function sendSetPicture($jid, $filepath) { - if(stripos($filepath, 'http')!== false && !preg_match('/\s/',$filepath)){ - $extension = end(explode(".", $filepath)); - $newImageName = rand(0, 100000); - $imagePath = static::PICTURES_FOLDER."/".$newImageName.".jpg"; - if($extension == 'jpg'){ - copy($filepath, $imagePath); - $filepath = $imagePath; - } - } - preprocessProfilePicture($filepath); - $fp = @fopen($filepath, "r"); - if ($fp) { - $data = fread($fp, filesize($filepath)); - if ($data) { - //this is where the fun starts - $picture = new ProtocolNode("picture", null, null, $data); - - $icon = createIconGD($filepath, 96, true); - $thumb = new ProtocolNode("picture", array("type" => "preview"), null, $icon); - - $hash = array(); - $nodeID = $this->createMsgId("setphoto"); - $hash["id"] = $nodeID; - $hash["to"] = $this->getJID($jid); - $hash["type"] = "set"; - $hash["xmlns"] = "w:profile:picture"; - $node = new ProtocolNode("iq", $hash, array($picture, $thumb), null); - - $this->sendNode($node); - $this->waitForServer($nodeID); - } - } + $nodeID = $this->createIqId(); + + $data = preprocessProfilePicture($filepath); + $preview = createIconGD($filepath, 96, true); + + $picture = new ProtocolNode('picture', ['type' => 'image'], null, $data); + $preview = new ProtocolNode('picture', ['type' => 'preview'], null, $preview); + + $node = new ProtocolNode('iq', [ + 'id' => $nodeID, + 'to' => $this->getJID($jid), + 'type' => 'set', + 'xmlns' => 'w:profile:picture', + ], [$picture, $preview], null); + + $this->sendNode($node); } + /** - * Parse the message text for emojis - * - * This will look for special strings in the message text - * that need to be replaced with a unicode character to show - * the corresponding emoji. - * - * Emojis should be entered in the message text either as the - * correct unicode character directly, or if this isn't possible, - * by putting a placeholder of ##unicodeNumber## in the message text. - * Include the surrounding ## - * eg: - * ##1f604## this will show the smiling face - * ##1f1ec_1f1e7## this will show the UK flag. - * - * Notice that if 2 unicode characters are required they should be joined - * with an underscore. - * - * - * @param string $txt - * The message to be parsed for emoji code. + * @param string $jid * * @return string */ - private function parseMessageForEmojis($txt) + private function parseJID($jid) { - $matches = null; - preg_match_all('/##(.*?)##/', $txt, $matches, PREG_SET_ORDER); - if (is_array($matches)) { - foreach ($matches as $emoji) { - $txt = str_ireplace($emoji[0], $this->unichr((string) $emoji[1]), $txt); + $parts = explode('@', $jid); + $parts = reset($parts); + + return $parts; + } + + public function sendPendingMessages($jid) + { + if ($this->messageStore != null && $this->isLoggedIn()) { + $messages = $this->messageStore->getPending($jid); + foreach ($messages as $message) { + $this->sendMessage($message['to'], $message['message']); } } - - return $txt; } /** - * Creates the correct unicode character from the unicode code point + * @param $number * - * @param int $int - * @return string + * @return \SessionCipher */ - private function unichr($int) + public function getSessionCipher($number) { - $string = null; - $multiChars = explode('_', $int); + if (!isset($this->sessionCiphers[$number])) { + $this->sessionCiphers[$number] = new SessionCipher($this->axolotlStore, $this->axolotlStore, $this->axolotlStore, $this->axolotlStore, $number, 1); + } - foreach ($multiChars as $char) { - $string .= mb_convert_encoding('&#' . intval($char, 16) . ';', 'UTF-8', 'HTML-ENTITIES'); + return $this->sessionCiphers[$number]; + } + + /** + * @param $groupId + * + * @return \GroupCipher + */ + public function getGroupCipher($groupId) + { + if (!isset($this->groupCiphers[$groupId])) { + $this->groupCiphers[$groupId] = new GroupCipher($this->axolotlStore, $groupId); } - return $string; + return $this->groupCiphers[$groupId]; + } + + public function getMyNumber() + { + return $this->phoneNumber; + } + + public function getReadReceipt() + { + return $this->readReceipts; + } + + public function getNodeId() + { + return $this->nodeId; + } + + public function getv2Jids() + { + return $this->v2Jids; + } + + public function setv2Jids($author) + { + $this->v2Jids[] = $author; + } + + public function setRetryCounter($id, $counter) + { + $this->retryCounters[$id] = $counter; + } + + public function setGroupId($id) + { + $this->groupId = $id; + } + + public function setMessageId($id) + { + $this->messageId = $id; + } + + public function getChallengeData() + { + return $this->challengeData; + } + + public function setChallengeData($data) + { + $this->challengeData = $data; + } + + public function setOutputKey($outputKey) + { + $this->outputKey = $outputKey; + } + + public function getLoginStatus() + { + return $this->loginStatus; + } + + public function getPendingNodes() + { + return $this->pending_nodes; + } + + public function unsetPendingNode($jid) + { + unset($this->pending_nodes[ExtractNumber($jid)]); + } + + public function getNewMsgBind() + { + return $this->newMsgBind; + } + + public function getMessageStore() + { + return $this->messageStore; } /** - * @param string $jid - * @return string + * @return \axolotlInterface */ - public static function parseJID($jid) + public function getAxolotlStore() { - $parts = explode('@', $jid); - $parts = reset($parts); - return $parts; + return $this->axolotlStore; + } + + public function pushMessageToQueue($node) + { + array_push($this->messageQueue, $node); + } + + /** + * @return mixed + */ + public function getLastId() + { + return $this->lastId; + } + + /** + * @param mixed $lastId + * + * @return WhatsProt + */ + public function setLastId($lastId) + { + $this->lastId = $lastId; + + return $this; } } diff --git a/tests/RegistrationTest.php b/tests/RegistrationTest.php new file mode 100644 index 00000000..e1629e3b --- /dev/null +++ b/tests/RegistrationTest.php @@ -0,0 +1,78 @@ +assertTrue(file_exists($expectedFile)); + } + + /** + * This is the current behaviour, when the filepath is the id file + * instead of a directory (a common workaround for custom files) + * + * @covers Registration::__construct + * @covers Registration::buildIdentity + */ + public function testRegistrationIdentityfileCanBeCreatedWithFile() + { + $number = '1234567890'; + $filePath = __DIR__ . '/_files/id.' . $number . '.dat'; + + require_once __DIR__ . '/../src/Registration.php'; + $registration = new Registration($number, false, $filePath); + + $expectedFile = $filePath; + + $this->assertTrue(file_exists($expectedFile)); + } + + /** + * The current behaviour, now creating the identity file when + * only the path was provided (as the code flow suggests). + * + * @covers Registration::__construct + * @covers Registration::buildIdentity + */ + public function testRegistrationIdentityfileCanBeCreatedWithPath() + { + $number = '1234567890'; + $filePath = __DIR__ . '/_files'; + + require_once __DIR__ . '/../src/Registration.php'; + $registration = new Registration($number, false, $filePath); + + $expectedFile = sprintf('%s/id.%s.dat', $filePath, $number); + + $this->assertTrue(file_exists($expectedFile)); + } +} diff --git a/tests/TestWhatsProt.php b/tests/TestWhatsProt.php index 086105c9..977f1e19 100755 --- a/tests/TestWhatsProt.php +++ b/tests/TestWhatsProt.php @@ -1,11 +1,14 @@ eventManager()->addEventListener($listener); - -// Old event bindings: -$old_function1_called = array(); -$old_function2_called = array(); - -// Function bound to variable: -$oldFunction = function( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $message -) use (&$old_function1_called) { - array_push($old_function1_called, array( - 'onGetMessage', - array( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $message - ) - ) ); -}; - -// Named funciton: -function oldFunction2( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $message -) { - global $old_function2_called; - - array_push($old_function2_called, array( - 'onGetMessage', - array( - $phone, - $from, - $msgid, - $type, - $time, - $name, - $message - ) - ) ); -}; - -// Callback approach: -$w->eventManager()->bind('onGetMessage', $oldFunction ); -// String approach: -$w->eventManager()->bind('onGetMessage', 'oldFunction2' ); - -print( "Test #1: onGetMessage\n" ); -/* -rx -rx -rx -rx TestMessage -rx -*/ - -// To be moved to config: -$node = new ProtocolNode("message",array( - 'from' => '441234123456', - 'id' => '1234567890-123', - 'type' => 'chat', - 't' => '1234567890' -), array( - new ProtocolNode('notify', array('xmlns'=>'urn:xmpp:whatsapp','name'=>'First LastName'), NULL, ''), - new ProtocolNode('request', array('xmlns'=>'urn:xmpp:receipts'), NULL, ''), - new ProtocolNode('body', NULL, NULL, 'TestMessage') -), '' ); - -// Send the data into the framework: -$w->processInboundDataNode($node); - -// Analyze the results: -$actual = $listener->getAndResetCapture(); -// Assert expected result: -$expected = array( - //First event raised: - array( - 'onSendMessageReceived', - array( - 0 => '$username', - // 1 = This is the current time. - 2 => '441234123456' - ) - ), - //Second event raised: - array( - // Event name: - 'onGetMessage', - // Event Arguments: - array( - '$username', - '441234123456', - '1234567890-123', - 'chat', - '1234567890', - 'First LastName', - 'TestMessage' - ) - ) -); -$old_expected = array($expected[1]); - -unset($actual[0][1][1]); // Remove the time. - -// Analyze the results: -if( $expected === $actual - && $old_expected === $old_function1_called - && $old_expected === $old_function2_called ) { - print( "Test 1 Passed.\n"); -} else { - print( "Test 1 Failed!!!!!\n" ); -} -$old_function1_called = array(); -$old_function2_called = array(); - -$expected = array( - //First event raised: - array( - // Event name: - 'onGetMessage', - // Event Arguments: - array( - '$username', - '441234123456', - '1234567890-123', - 'chat', - '1234567890', - 'First LastName', - 'TestMessage' - ) - ) -); -$old_expected = $expected; - -print( "Test #2: Legacy Fire\n" ); -$w->eventManager()->fire('onGetMessage', array( - '$username', - '441234123456', - '1234567890-123', - 'chat', - '1234567890', - 'First LastName', - 'TestMessage' -) ); - -// Analyze the results: -$actual = $listener->getAndResetCapture(); -if( $expected === $actual - && $old_expected === $old_function1_called - && $old_expected === $old_function2_called ) { - print( "Test 2 Passed.\n"); -} else { - print( "Test 2 Failed!!!!!\n" ); -} -$old_function1_called = array(); -$old_function2_called = array(); - - -print( "Test #3: onGetGroupMessage\n" ); -/* -rx -rx -rx -rx Are you real, or are you a bot? -rx -*/ - -$node = new ProtocolNode("message",array( - 'from' => '441234123456-1234567890@g.us', - 'id' => '1234567890-123', - 'type' => 'chat', - 't' => '1234567890', - 'author' => '11231231234@s.whatsapp.net' -), array( - new ProtocolNode('notify', array('xmlns'=>'urn:xmpp:whatsapp','name'=>'Fun Guy'), NULL, ''), - new ProtocolNode('request', array('xmlns'=>'urn:xmpp:receipts'), NULL, ''), - new ProtocolNode('body', NULL, NULL, 'Are you real, or are you a bot?') -), '' ); -// Send the data into the framework: -$w->processInboundDataNode($node); -// Analyze the results: -$actual = $listener->getAndResetCapture(); -// Assert expected result: -$expected = array( - //First event raised: - array( - 'onSendMessageReceived', - array( - 0 => '$username', - // 1 = This is the current time. - 2 => '441234123456-1234567890@g.us' - ) - ), - // Second event raised: - array( - // Event name: - 'onGetGroupMessage', - // Event Arguments: - array( - '$username', - '441234123456-1234567890@g.us', - '11231231234@s.whatsapp.net', - '1234567890-123', - 'chat', - '1234567890', - 'Fun Guy', - 'Are you real, or are you a bot?' - ) - ) -); -unset($actual[0][1][1]); // Remove the time. - -if( $expected === $actual ) { - print( "Test 3 Passed.\n"); -} else { - print( "Test 3 Failed!!!!!\n" ); -} - - -?> \ No newline at end of file diff --git a/tests/WhatsAppEventListenerCapture.php b/tests/WhatsAppEventListenerCapture.php deleted file mode 100755 index 6ff96241..00000000 --- a/tests/WhatsAppEventListenerCapture.php +++ /dev/null @@ -1,20 +0,0 @@ -capture, array($eventName,$arguments)); - } - - public function getAndResetCapture() { - $ret = $this->capture; - $this->capture = array(); - return $ret; - } -} diff --git a/tests/whatsapp.php b/tests/whatsapp.php index 23197d11..5a60c7a4 100755 --- a/tests/whatsapp.php +++ b/tests/whatsapp.php @@ -1,30 +1,28 @@ -#!/usr/bin/php 0) { return trim(fgets($pStdn, 1024)); } - return null; } -$nickname = "WhatsAPI Test"; +$nickname = 'WhatsAPI Test'; // #### DO NOT ADD YOUR INFO AND THEN COMMIT THIS FILE! #### -$sender = ""; // Mobile number with country code (but without + or 00) -$identity = ""; // Obtained during registration -$password = ""; // Password you received from WhatsApp +$sender = ''; // Mobile number with country code (but without + or 00) +$password = ''; // Password you received from WhatsApp if ($argc < 2) { - echo "USAGE: ".$_SERVER['argv'][0]." [-l] [-s ] [-i ] [-set ]\n"; + echo 'USAGE: '.$_SERVER['argv'][0]." [-l] [-s ] [-i ] [-set ]\n"; echo "\tphone: full number including country code, without '+' or '00'\n"; echo "\t-s: send message\n"; echo "\t-l: listen for new messages\n"; @@ -33,47 +31,43 @@ function fgets_u($pStdn) exit(1); } -$dst=$_SERVER['argv'][2]; -$msg = ""; -for ($i=3; $i<$argc; $i++) { - $msg .= $_SERVER['argv'][$i]." "; +$dst = $_SERVER['argv'][2]; +$msg = ''; +for ($i = 3; $i < $argc; $i++) { + $msg .= $_SERVER['argv'][$i].' '; } echo "[] Logging in as '$nickname' ($sender)\n"; -$wa = new WhatsProt($sender, $imei, $nickname, TRUE); +$wa = new WhatsProt($sender, $nickname, true); $wa->connect(); $wa->loginWithPassword($password); -if ($_SERVER['argv'][1] == "-i") { +if ($_SERVER['argv'][1] == '-i') { echo "\n[] Interactive conversation with $dst:\n"; - stream_set_timeout(STDIN,1); - while (TRUE) { - while($wa->pollMessage()); + stream_set_timeout(STDIN, 1); + while (true) { + while ($wa->pollMessage()); $buff = $wa->getMessages(); if (!empty($buff)) { print_r($buff); } $line = fgets_u(STDIN); - if ($line != "") { - if (strrchr($line, " ")) { + if ($line != '') { + if (strrchr($line, ' ')) { // needs PHP >= 5.3.0 - $command = trim(strstr($line, ' ', TRUE)); + $command = trim(strstr($line, ' ', true)); } else { $command = $line; } switch ($command) { - case "/query": - $dst = trim(strstr($line, ' ', FALSE)); + case '/query': + $dst = trim(strstr($line, ' ', false)); echo "[] Interactive conversation with $dst:\n"; break; - case "/lastseen": - echo "[] Request last seen $dst: "; - $wa->sendGetRequestLastSeen($dst); - break; default: echo "[] Send message to $dst: $line\n"; - $wa->sendMessage($dst , $line); + $wa->sendMessage($dst, $line); break; } } @@ -81,26 +75,25 @@ function fgets_u($pStdn) exit(0); } -if ($_SERVER['argv'][1] == "-l") { +if ($_SERVER['argv'][1] == '-l') { echo "\n[] Listen mode:\n"; - while (TRUE) { + while (true) { $wa->pollMessage(); $data = $wa->getMessages(); - if(!empty($data)) print_r($data); + if (!empty($data)) { + print_r($data); + } sleep(1); } exit(0); } -if ($_SERVER['argv'][1] == "-set") { +if ($_SERVER['argv'][1] == '-set') { echo "\n[] Setting status:\n"; $wa->sendStatusUpdate($_SERVER['argv'][2]); exit(0); } -echo "\n[] Request last seen $dst: "; -$wa->sendGetRequestLastSeen($dst); - echo "\n[] Send message to $dst: $msg\n"; -$wa->sendMessage($dst , $msg); -echo "\n"; \ No newline at end of file +$wa->sendMessage($dst, $msg); +echo "\n";