diff options
author | / <> | 2021-02-16 21:42:53 +0300 |
---|---|---|
committer | / <> | 2021-02-16 21:42:53 +0300 |
commit | 25cbf6d1524b823d710cdd323d001cc15be72d58 (patch) | |
tree | fe00a9c19960ece9688a3adcbfaa8c15ca43f82b | |
parent | 1a6711f83d62e374ca2bed41fd4ca25b6bc654a2 (diff) | |
parent | a13cb8b1b85089c823917d4bf2b89a89f067393e (diff) |
merged hg history of libpurple
26 files changed, 19125 insertions, 0 deletions
diff --git a/pidgin/AUTHORS b/pidgin/AUTHORS new file mode 100644 index 0000000..a0da492 --- /dev/null +++ b/pidgin/AUTHORS @@ -0,0 +1,146 @@ +Pidgin and Finch: The Pimpin' Penguin IM Clients That're Good for the Soul +========================================================================== + +For a complete list of all contributors, see the COPYRIGHT file. + +We've got an IRC room now too, #pidgin on irc.freenode.net. Come check us out. + +Current Developers: +------------------ +Daniel 'datallah' Atallah - Developer +Paul 'darkrain42' Aurich - Developer +Ethan 'Paco-Paco' Blanton - Developer +Sadrul Habib Chowdhury - Developer +Gary 'grim' Kramlich - Maintainer +Richard 'rlaager' Laager - Developer +Marcus 'malu' Lundblad - Developer +Sulabh 'sulabh_m' Mahajan - Developer +Richard 'wabz' Nelson - Developer +Etan 'deryni' Reisner - Developer +Michael 'Maiku' Ruprecht - Developer, voice and video +Elliott 'QuLogic' Sales de Andrade - Developer +Evan Schoenberg - Developer +Kevin 'SimGuy' Stange - Developer & Webmaster +Will 'resiak' Thompson - Developer +Stu 'nosnilmot' Tomlinson - Developer +Jorge 'Masca' Villaseñor - Developer +Tomasz Wasilczyk - Developer + +Crazy Patch Writers: +------------------- +Jakub 'haakon' Adam +James 'jgeboski' Geboski +Krzysztof Klinikowski +Eion Robb +Ankit Vani + +Retired Developers: +------------------ +John 'rekkanoryo' Bailey - Developer +Herman Bloggs - Win32 Port +Thomas Butter - Developer +Ka-Hing Cheung - Developer +Mark 'KingAnt' Doliner - maintainer +Jim Duchek <jim@linuxpimps.com> - maintainer +Sean Egan - Developer +Rob Flynn <gaim@robflynn.com> - maintainer +Adam Fritzler - libfaim maintainer +Christian 'ChipX86' Hammond - Developer & Webmaster +Casey Harkins - Developer +Ivan Komarov - Developer +Syd Logan - hacker and designated driver [lazy bum] +Christopher 'siege' O'Brien - Developer +Bartosz Oler - Developer +Tim 'marv' Ringenbach - Developer +Luke 'LSchiere' Schierer - Support +Megan 'Cae' Schneider - support/QA +Jim Seymour - XMPP developer +Mark Spencer <markster@marko.net> - original author +Nathan 'faceprint' Walp - Developer +Eric Warmenhoven <eric@warmenhoven.org> - lead developer + +Retired Crazy Patch Writers: +--------------------------- +Felipe 'shx' Contreras +Decklin Foster +Dennis 'EvilDennisR' Ristuccia - Senior Contributor/QA +Peter 'Bleeter' Lawler +Robert 'Robot101' McQueen +Benjamin Miller +Peter 'Fmoo' Ruibal +Gabriel 'Nix' Schulhof + +Artists: +------- +Hylke Bons - Icons + +Other Contributions: +------------------- +Much thanks to Evan Martin <martine@cs.washington.edu> for writing +GtkSpell <http://gtkspell.sourceforge.net> responsible for the +"Highlight misspelled words" feature and for gtk-nativewin +<http://bunny.darktech.org/cvs/gtk-nativewin/> the default GTK+-2.0 +engine originally used in our win32 port. + +** ORIGINAL LOGO DESIGNED BY: Naru Sundar ** + +Peter Teichiman <peter@helixcode.com> +Larry Ewing +Jeramey A. Crawford + Thanks to these boys. Peter and Larry managed to stomp + out a large list of Mem Leaks. Jeramey found the remaining + onees and pointed me to those. Props to the boys at + Helix Code. Thanks guys. + +Nathan Walp + A healthy amount of patches for the Jabber plugin + +Neil Sanchala + Wrote most of the Zephyr plugin + +Arkadiusz Miskiewicz + Wrote the Gadu-Gadu plugin + +David Prater <IM: dRaven43> draven@tcsx.net + Log and Colour Button Images + +Sébastien Carpe <IM: Seb Carpe> + Base HTTP Proxy Support + +Ari Pollak <IM: Ari Pollak> compwiz.dhs.org + Resize conversation window patch + +Decklin Foster + Many GUI improvements, other nifty additions and fixes + +David <IM: CrazyDavy> + The neato-bigger text box + +S D Erle + Writing a cool perl script to translate WinAIM lists to gaim + +BMiller + A good collection of stuff. %n for away messages, import winaim + lists, pic/text/pic+text for buttons, among others + +Lance Rocker + Improved HTML formatting in logs, plus lots of debugging on *BSD. + +ergofobe: + GNOME Url handler patch + +Justin M. Ward <justin@yossman.net>: + Alphabetical Away Messages patch + +G. Sumner Hayes <IM: SumnerFool> Security Patches + +Brian Ryner for a little make file patch :) + +Ryan C. Gordon - I still think you look like Silent Bob. + +Elliot Tobin <elliot@bha.udel.edu> + +Thanks to Jeroen van der Vegt for the initial smiley plugin and images. + +The OpenQ Team + Wrote the QQ plugin dropped in 2.8.0 (see libpurple/qq/AUTHORS in 2.7.11) diff --git a/pidgin/COPYING b/pidgin/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/pidgin/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and 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 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This 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. diff --git a/pidgin/COPYRIGHT b/pidgin/COPYRIGHT new file mode 100644 index 0000000..998324c --- /dev/null +++ b/pidgin/COPYRIGHT @@ -0,0 +1,643 @@ +Pidgin, Finch, and libpurple + +This file is intended to be a comprehensive list of contributors to +this project. If you have contributed to this project then you deserve +to be on this list. Contact us (see: AUTHORS) and we'll add you. + +Many open source projects list contributor names at the top of each +source file containing their contribution. However, we've found +that it is difficult to keep this list accurate, especially when old +code is removed or existing code is moved to a different file. So +instead we chose to list a generic message at the top of each source +file that points here. + +If concerns are raised as to the copyright holder of a particular +piece of code, then that code should be traced through our version +control system to see from where it came and who has modified it. + +Copyright (C) 1998-2014 by the following: + +Mark +Saleem Abdulrasool +Jakub Adam +Dave Ahlswede +Haval A. Ahmed +Sorokin Alexei +Thijs Alkemade +Manuel Amador +Matt Amato +Josef Andrysek +Flavius Anton +Geoffrey Antos +Daniel Atallah +Paul Aurich +Patrick Aussems +Anibal Avelar +Ali Albazaz +Kosta Arvanitis +Christopher Ayoup +Alex Badea +John Bailey +Arunan Balasubramaniam +R. Tyler Ballance +Chris Banal +Luca Barbato +Levi Bard +Mark Barfield +Ryan Barrett +Kevin Barry +Lukas Barth +Derek Battams +Martin Bayard +Curtis Beattie +Stefan Becker +Carlos Bederian +Dave Bell +Matthew W.S. Bell +Igor Belyi +David Benjamin +Brian Bernas +Vivien Bernet-Rollande +Paul Betts +Runa Bhattacharjee +Jonas Birmé +George-Cristian Bîrzan +Eric Blade +Ethan Blanton +Joshua Blanton +Rainer Blessing +Herman Bloggs +David Blue +Jason Boerner +Hylke Bons +Graham Booker +Paolo Borelli +Julien Bossart +Craig Boston +Éric Boumaour +Chris Boyle +Stanislav Brabec +Bartosz Brachaczek +Quentin Brandon +Derrick J Brashear +Mauro Sérgio Ferreira Brasil +Luke Bratch +Matt Brenneke +Jeremy Brooks +Jonathan Brossard +Jeffery Brown +Philip Brown +Dan Bruce +Guillaume Brunerie +Norbert Buchmuller +Johannes Buchner +Sean Burke +Gabriel Burt +Thomas Butter +Trevor Caira +Andrea Canciani +Damien Carbery +Michael Carlson +Rodrigo Tobar Carrizo +Keegan Carruthers-Smith +Ludovico Cavedon +Steve Cavilia +Julien Cegarra +Matěj Cepl +Cerulean Studios, LLC +Jonathan Champ +Markos Chandras +Matthew Chapman +Christophe Chapuis +Tirtha Chatterjee +Patrick Cheung +Ka-Hing Cheung +Sadrul Habib Chowdhury +Brian Chu +Howard Chu +Arturo Cisneros, Jr. +Vincas Ciziunas +Jonathan Clark +Joe Clarke +Eoin Coffey +Jason Cohen +Todd Cohen +Graham Cole +Jono Cole +Lorenzo Colitti +Collabora Ltd. +Jeff Connelly +Chris Connett +Nathan Conrad +Felipe Contreras +Alex Converse +Irving Cordova +Glauber de Oliveira Costa +Adam Cowell +Palmer Cox +Jeramey Crawford +Olivier Crete +Michael Culbertson +Steven Danna +Simon Danner +Chris Davies +Josh Davis +Martijn Dekker +Florian Delizy +Jiri Denemark +Vinicius Depizzol +Marc Dequènes +Philip Derrin +Taso N. Devetzis +Balwinder Singh Dheeman +Chandrakant Dhutadmal +Andrew Dieffenbach +Ingmārs Dīriņš +Finlay Dobbie +Mark Doliner +Nuno Donato +Jim Duchek +Alex Duggan +Tom Dyas +Marc E. +Andrew Echols +John Eckerdal +Sean Egan <seanegan@gmail.com> +William Ehlhardt +Markus Elfring +Nelson Elhage +Ignacio J. Elia +Kai Engert +Brian Enigma +Mattias Eriksson +Pat Erley +Stefan Esser +Steffen Eschenbacher +Marc Etcheverry +David Everly +Larry Ewing +Facebook, Inc. +Fartash Faghri +Gábor Farkas +Jesse Farmer +Gavan Fantom (gavan) +Leonardo Fernandes +David Fiander +Michael Fiedler +Ryan Flegel +Rob Flynn <gaim@robflynn.com> +Rob Foehl (rwf) +Chris Foote +Alan Ford +Nathan Fredrickson +Chris J. Friesen +Free Software Foundation +Decklin Foster +Francesco Fracassi +Adam Fritzler +Takao Fujiwara +Max G. +Martin von Gagern +François Gagné +Andrew Gaul +Evgueni V. Gavrilov +Ignacy Gawedzki +James Geboski +Georgi Georgiev +Brian Geppert +Emanuele Giaquinta +Thomas Gibson-Robinson +Ike Gingerich +Gustavo Giráldez +Richard Gobeille +Ian Goldberg +Jon Goldberg +Matthew Goldstein +Michael Golden +Issa Gorissen +Charlie Gordon +Ryan C. Gordon +Konrad Gräfe +Miah Gregory +David Grohmann +Christian Grothoff +Vladislav Guberinić +Gideon N. Guillen +Aman Gupta +Ashish Gupta +Christian Hammond +Erick Hamness +Fred Hampton +John Hanauer +Phil Hannent +Casey Harkins +Andy Harrison +Andrew Hart (arhart) +Anders Hasselqvist +Rene Hausleitner +Will Hawkins +G. Sumner Hayes +Michael R. Head +Nick Hebner +Mike Heffner +Justin Heiner +Moos Heintzen +Benjamin Herrenschmidt +Fernando Herrera +hjheins +Hil +Casey Ho +Andrew Hoffman +Iain Holmes +Joshua Honeycutt +Jeffrey Honig +Nigel Horne +Jensen Hornick +Juanjo Molinero Horno +Dustin Howett +Nathanael Hoyle +Greg Hudson +Magnus Hult +Karsten Huneycutt +Andrew Hunt +Kevin Hunter +Rian Hunter +Thomas Huriaux +Instant Messaging Freedom, Inc. +Vitaliy Ischenko +Intel Corporation +Andrew Ivanov +Momchil Ivanov +Scott Jackson +Hans Petter Jansson +David Jedelsky +Henry Jen +Benjamin Kahn +Jan Kaluza +Yuriy Kaminskiy +Anders Kaseorg +Praveen Karadakal +Tomáš Kebert +John Kelm +Jochen Kemnade +Yann Kerherve +Akmal Khushvakov +Gordian Klein +Marten Klencke +Krzysztof Klinikowski +KNTRO +Akuke Kok +Kir Kolyshkin +Ivan Komarov +F.W. Kong +Konstantin Korikov +Cole Kowalski +Nikita Kozlov +Matt Kramer +Gary Kramlich +Jan Kratochvil +Andrej Krivulčík +Patrik Kullman +Sangeeta Kumari +Tuomas Kuosmanen +Tero Kuusela +Richard Laager +Jacky Lam +Scott Lamb +Dennis Lambe Jr. +Joe LaPenna +Steve Láposi +Daniel Larsson +Julia Lawall +Peter Lawler +Vadim Lebedev +Ho-seok Lee +Jean-Yves Lefort +Moses Lei +Ambrose C. Li +Nicolas Lichtmaier +Wesley Lin +Shaun Lindsay +Artem Litvinovich +Josh Littlefield +Daniel Ljungborg +Syd Logan +Lokheed +Norberto Lopes +Shlomi Loubaton +Pieter Loubser +Brian Lu +Uli Luckas +Matthew Luckie +Marcus Lundblad +Mike Lundy +Jason Lynch +Iain MacDonnell +Lucio Maciel +Brian Macke +Paolo Maggi +Sulabh Mahajan +Willian T. Mahan +Jonathan Maltz +Rok Mandeljc +Tobias Markmann +Kris Marsh +Fidel Martinez +Lalo Martins +John Matthews +Simo Mattila +Robert Matusewicz +Michal Matyska +Rudolfs Mazurs +Ryan McCabe +Peter McCurdy +Kurt McKee +James McLaughlin +Torrey McMahon +Greg McNew +Robert McQueen +Mihály Mészáros +Robert Mibus +David Michael +Lars T. Mikkelsen +Mantas Mikulėnas +Benjamin Miller +Kevin Miller +Paul Miller +Arkadiusz Miskiewicz +David Mohr +Kartik Mohta +Andrew Molloy +Tomasz Mon +Michael Monreal +Laurent Montaron +Marco Monteiro +Benjamin Moody +John Moody +Tim Mooney +Sergio Moretto +Nader Morshed +Keith Moyer +Andrei Mozzhuhin +Christian Muise +MXit Lifestyle (Pty) Ltd. +Alexander Nartov +Richard Nelson +Dennis Nezic +Matthew A. Nicholson +Andreas Nilsson +Allan Nordhøy +Henning Norén +Szilard Novaki +Novell +Padraig O'Briain +Christopher O'Brien (siege) +Peter O'Gorman +Jon Oberheide +Marcos García Ochoa +Yusuke Odate +Ruediger Oertel +Gudmundur Bjarni Olafsson +Bartosz Oler +Oliver +The openSUSE Project +Jürgen Orschiedt +Stefan Ott +Shawn Outman +Nathan Owens (pianocomp81) +John Oyler +Matt Pandina +Laszlo Pandy +Giulio 'Twain28' Pascali +Ricardo Fernandez Pascual +Riley Patterson +Havoc Pennington +Ted Percival +Hugo Pereira Da Costa +Eduardo Pérez +Matt Perry +Ani Peter +Luke Petre +Diego Petten +Nathan Peterson +Dmitry Petroff +Sebastián E. Peyrott +Amitakhya Phukan +Andrea Piccinelli +Mateusz Piękos +Celso Pinto +Joao Luís Marques Pinto +Aleksander Piotrowski +Julien Pivotto +Robey Pointer +Eric Polino <aluink@gmail.com> +Ari Pollak +Stephen Pope +Cristi Posoiu +Alexei Potashnik +Nathan Poznick +Jory A. Pratt +David Preece +Brent Priddy +Justin Pryzby +Florian Quèze +Ignacio Casal Quinteiro +Federicco Mena Quintero +Yosef Radchenko +David Raeman +R. Ramkumar +Rajesh Ranjan +Mart Raudsepp +Etan Reisner +David Reiss +Luoh Ren-Shan +Noa Resare +Tim Retout +Daniele Ricci +Kristian Rietveld +Pekka Riikonen +Tim Ringenbach +Dennis Ristuccia +Lee Roach +Eion Robb +Kahlil Robinson +Rhett Robinson +Luciano Miguel Ferreira Rocha +Andrew Rodland +Miguel Rodríguez (migrax) +Adi Roiban +Martin Rosinski +Bob Rossi +Jason Roth +Jean-Francois Roy +Peter Ruibal +Michael Ruprecht +Sam S. +Thanumalayan S. +Jonathan Sailor +Elliott Sales de Andrade +Catalin Salgau +Tomasz Sałaciński <tsalacinski@gmail.com> +Pradyumna Sampath +Arvind Samptur +Tom Samstag +Neil Sanchala +Laurent Sansonetti +Andrew Sayman +Alceste Scalas +Carsten Schaar +Toby Schaffer +Jonathan Schleifer <js-pidgin@webkeks.org> +Luke Schierer +Sebastian Schmidt <yath@yath.de> +Ralph Schmieder +David Schmitt +Heiko Schmitt +Mark Schneider +Evan Schoenberg +Gabriel Schulhof +Eric Michael Schwelm +Federico Schwindt +Torrey Searle +Peter Seebach +Don Seiler +Mihai Serban +Leonardo Serra +Matteo Settenvini +Colin Seymour +Jim Seymour +Javeed Shaikh +Joe Shaw +Scott Shedden +Dossy Shiobara +Michael Shkutkov +Shreevatsa R +Dylan Simon <dylan@dylex.net> +Ettore Simone +Renato Silva +John Silvestri +Mukund Sivaraman +Craig Slusher +Alex Smith +Brad Smith +Malcolm Smith +David Smock +Phil Snowberger +Eddie Sohn (tr1sk) +Sony Computer Entertainment America, Inc. +Andy Spencer +Mark Spencer +Peter Speybrouck +Lex Spoon +Chris Stafford +Kevin Stange +Ferdinand Stehle +Joshua Stein +Jakub Steiner +Richard Stellingwerff +Charlie Stockman +David Stoddard +Adam Strzelecki +Andreas Stührk +Oleg Sukhodolsky +Sun Microsystems +Marcus Sundberg +Mårten Svantesson (fursten) +Amir Szekely (kichik) +Gábor Szuromi (kukkerman) +Jakub Szypulka +Robert T. +Greg Taeger +Rob Taft +Peter Tang +Brian Tarricone +Peter Teichman +Philip Tellis +Michael Terry +Arun A. Tharuvai +Cestonaro Thilo +Will Thompson +Douglas Thrift (douglaswth) +Niels Thykier +Mark Tiefenbruck +Andrew Tinney +Jeffery To +Krzysztof Tobola (kreez) +Warren Togami +Stu Tomlinson +Bill Tompkins +Gal Topper +Chris Toshok +Ken Tossell +Marcus Trautwig +Tom Tromey +Todd Troxell +Brad Turcotte +Kyle Turman +Jon Turney +Junichi Uekawa +Max Ulidtko +Dmitry Utkin +Igor Vlasenko +István Váradi +ILDAR Valeev +Cédric Valmary +Martijn van Beers +Gideon van Melle +Arjan van de Ven +Philip Van Hoof +Ankit Vani +Kristof Vansant +James Vega +David Vermeille +Sid Vicious +Andrew Victor +Jorge Villaseñor (Masca) +Bjoern Voigt +Peter Volkov +Marius Wachtler +Wan Hing Wah +Philip Walford +Nathan Walp +Jonty Wareing +Eric Warmenhoven +Adam J. Warrington +Denis Washington +Tomasz Wasilczyk +Zsombor Welker +Andrew Wellington +Adam Wendt +Simon Wenner +Dave West +Zac West +Daniel Westermann-Clark +Andrew Whewell +Stephen Whitmore +Simon Wilkinson +Dan Willemsen +Dan Williams +Justin Williams (Jaywalker) +Jason Willis +Alex Willmer +Matt Wilson +Dan Winship +Michal Witkowski +Scott Wolchok +Rogier Wolff +The Written Word, Inc. +Kevin Wu Won +Pui Lam Wong +Justin Wood +Ximian +Ma Xuan +Yonas Yanfa +Jared Yanovich +Timmy Yee +Li Yuan +Yuriy Yevgrafov +Jan Zachorowski +Nickolai Zeldovich +Tom Zickel +Marco Ziech +Piotr Zielinski +Jeroen Zwartepoorte diff --git a/pidgin/libpurple/glibcompat.h b/pidgin/libpurple/glibcompat.h new file mode 100644 index 0000000..ccc303d --- /dev/null +++ b/pidgin/libpurple/glibcompat.h @@ -0,0 +1,112 @@ +/* pidgin + * + * Pidgin is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _GLIBCOMPAT_H_ +#define _GLIBCOMPAT_H_ +/* + * SECTION:glibcompat + * @section_id: libpurple-glibcompat + * @short_description: <filename>glibcompat.h</filename> + * @title: GLib version-dependent definitions + * + * This file is internal to libpurple. Do not use! + * Also, any public API should not depend on this file. + */ + +#include <glib.h> + +/* glib's definition of g_stat+GStatBuf seems to be broken on mingw64-w32 (and + * possibly other 32-bit windows), so instead of relying on it, + * we'll define our own. + */ +#if defined(_WIN32) && !defined(_MSC_VER) && !defined(_WIN64) +# include <glib/gstdio.h> +typedef struct _stat GStatBufW32; +static inline int +purple_g_stat(const gchar *filename, GStatBufW32 *buf) +{ + return g_stat(filename, (GStatBuf*)buf); +} +# define GStatBuf GStatBufW32 +# define g_stat purple_g_stat +#endif + + +#ifdef __clang__ + +#undef G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#define G_GNUC_BEGIN_IGNORE_DEPRECATIONS \ + _Pragma ("clang diagnostic push") \ + _Pragma ("clang diagnostic ignored \"-Wdeprecated-declarations\"") + +#undef G_GNUC_END_IGNORE_DEPRECATIONS +#define G_GNUC_END_IGNORE_DEPRECATIONS \ + _Pragma ("clang diagnostic pop") + +#endif /* __clang__ */ + + +#if !GLIB_CHECK_VERSION(2, 44, 0) +#define G_IO_ERROR_CONNECTION_CLOSED G_IO_ERROR_BROKEN_PIPE +#endif + +/****************************************************************************** + * g_assert_* macros + *****************************************************************************/ +#if !GLIB_CHECK_VERSION(2, 38, 0) +#define g_assert_true(expr) G_STMT_START { \ + if G_LIKELY (expr) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should be TRUE"); \ + } G_STMT_END +#define g_assert_false(expr) G_STMT_START { \ + if G_LIKELY (!(expr)) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should be FALSE"); \ + } G_STMT_END +#define g_assert_null(expr) G_STMT_START { if G_LIKELY ((expr) == NULL) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should be NULL"); \ + } G_STMT_END +#endif + +#if !GLIB_CHECK_VERSION(2, 40, 0) +#define g_assert_nonnull(expr) G_STMT_START { \ + if G_LIKELY ((expr) != NULL) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should not be NULL"); \ + } G_STMT_END +#endif + +#if !GLIB_CHECK_VERSION(2, 46, 0) +#define g_assert_cmpmem(m1, l1, m2, l2) G_STMT_START {\ + gconstpointer __m1 = m1, __m2 = m2; \ + int __l1 = l1, __l2 = l2; \ + if (__l1 != __l2) \ + g_assertion_message_cmpnum (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + #l1 " (len(" #m1 ")) == " #l2 " (len(" #m2 "))", __l1, "==", __l2, 'i'); \ + else if (memcmp (__m1, __m2, __l1) != 0) \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "assertion failed (" #m1 " == " #m2 ")"); \ + } G_STMT_END +#endif + +#endif /* _GLIBCOMPAT_H_ */ diff --git a/pidgin/libpurple/http.c b/pidgin/libpurple/http.c new file mode 100644 index 0000000..fd88294 --- /dev/null +++ b/pidgin/libpurple/http.c @@ -0,0 +1,3406 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "http.h" + +#include "internal.h" +#include "glibcompat.h" + + +#include "debug.h" +#include "proxy.h" +#include "purple-gio.h" + +#define PURPLE_HTTP_URL_CREDENTIALS_CHARS "a-z0-9.,~_/*!&%?=+\\^-" +#define PURPLE_HTTP_MAX_RECV_BUFFER_LEN 10240 +#define PURPLE_HTTP_MAX_READ_BUFFER_LEN 10240 +#define PURPLE_HTTP_GZ_BUFF_LEN 1024 + +#define PURPLE_HTTP_REQUEST_DEFAULT_MAX_REDIRECTS 20 +#define PURPLE_HTTP_REQUEST_DEFAULT_TIMEOUT 30 +#define PURPLE_HTTP_REQUEST_DEFAULT_MAX_LENGTH 1048576 +#define PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH G_MAXINT32-1 + +#define PURPLE_HTTP_PROGRESS_WATCHER_DEFAULT_INTERVAL 250000 + +typedef struct _PurpleHttpSocket PurpleHttpSocket; + +typedef struct _PurpleHttpHeaders PurpleHttpHeaders; + +typedef struct _PurpleHttpKeepaliveHost PurpleHttpKeepaliveHost; + +typedef struct _PurpleHttpKeepaliveRequest PurpleHttpKeepaliveRequest; + +typedef struct _PurpleHttpGzStream PurpleHttpGzStream; + +typedef void (*PurpleHttpSocketConnectCb)(PurpleHttpSocket *hs, + const gchar *error, gpointer _hc); + +struct _PurpleHttpSocket +{ + GSocketConnection *conn; + GCancellable *cancellable; + guint input_source; + guint output_source; + + gboolean is_busy; + guint use_count; + PurpleHttpKeepaliveHost *host; +}; + +struct _PurpleHttpRequest +{ + int ref_count; + + gchar *url; + gchar *method; + PurpleHttpHeaders *headers; + PurpleHttpCookieJar *cookie_jar; + PurpleHttpKeepalivePool *keepalive_pool; + + gchar *contents; + int contents_length; + PurpleHttpContentReader contents_reader; + gpointer contents_reader_data; + PurpleHttpContentWriter response_writer; + gpointer response_writer_data; + + int timeout; + int max_redirects; + gboolean http11; + guint max_length; +}; + +struct _PurpleHttpConnection +{ + PurpleConnection *gc; + PurpleHttpCallback callback; + gpointer user_data; + gboolean is_reading; + gboolean is_keepalive; + gboolean is_cancelling; + + PurpleHttpURL *url; + PurpleHttpRequest *request; + PurpleHttpResponse *response; + + PurpleHttpKeepaliveRequest *socket_request; + PurpleHttpConnectionSet *connection_set; + PurpleHttpSocket *socket; + GString *request_header; + guint request_header_written, request_contents_written; + gboolean main_header_got, headers_got; + GString *response_buffer; + PurpleHttpGzStream *gz_stream; + + GString *contents_reader_buffer; + gboolean contents_reader_requested; + + int redirects_count; + + int length_expected; + guint length_got, length_got_decompressed; + + gboolean is_chunked, in_chunk, chunks_done; + int chunk_length, chunk_got; + + GList *link_global, *link_gc; + + guint timeout_handle; + + PurpleHttpProgressWatcher watcher; + gpointer watcher_user_data; + guint watcher_interval_threshold; + gint64 watcher_last_call; + guint watcher_delayed_handle; +}; + +struct _PurpleHttpResponse +{ + int code; + gchar *error; + + GString *contents; + PurpleHttpHeaders *headers; +}; + +struct _PurpleHttpURL +{ + gchar *protocol; + gchar *username; + gchar *password; + gchar *host; + int port; + gchar *path; + gchar *fragment; +}; + +struct _PurpleHttpHeaders +{ + GList *list; + GHashTable *by_name; +}; + +typedef struct +{ + time_t expires; + gchar *value; +} PurpleHttpCookie; + +struct _PurpleHttpCookieJar +{ + int ref_count; + + GHashTable *tab; +}; + +struct _PurpleHttpKeepaliveRequest +{ + PurpleConnection *gc; + PurpleHttpSocketConnectCb cb; + gpointer user_data; + + PurpleHttpKeepaliveHost *host; + PurpleHttpSocket *hs; +}; + +struct _PurpleHttpKeepaliveHost +{ + PurpleHttpKeepalivePool *pool; + + gchar *host; + int port; + gboolean is_ssl; + + GSList *sockets; /* list of PurpleHttpSocket */ + + GSList *queue; /* list of PurpleHttpKeepaliveRequest */ + guint process_queue_timeout; +}; + +struct _PurpleHttpKeepalivePool +{ + gboolean is_destroying; + + int ref_count; + + guint limit_per_host; + + /* key: purple_http_socket_hash, value: PurpleHttpKeepaliveHost */ + GHashTable *by_hash; +}; + +struct _PurpleHttpConnectionSet +{ + gboolean is_destroying; + + GHashTable *connections; +}; + +struct _PurpleHttpGzStream +{ + gboolean failed; + GZlibDecompressor *decompressor; + gsize max_output; + gsize decompressed; + GString *pending; +}; + +struct _ntlm_type1_message { + guint8 protocol[8]; /* 'N', 'T', 'L', 'M', 'S', 'S', 'P', '\0' */ + guint32 type; /* 0x00000001 */ + guint32 flags; /* 0x0000b203 */ + + guint16 dom_len1; /* domain string length */ + guint16 dom_len2; /* domain string length */ + guint32 dom_off; /* domain string offset */ + + guint16 host_len1; /* host string length */ + guint16 host_len2; /* host string length */ + guint32 host_off; /* host string offset (always 0x00000020) */ + +#if 0 + guint8 host[*]; /* host string (ASCII) */ + guint8 dom[*]; /* domain string (ASCII) */ +#endif +}; + +static time_t purple_http_rfc1123_to_time(const gchar *str); + +static gboolean purple_http_request_is_method(PurpleHttpRequest *request, + const gchar *method); + +static PurpleHttpConnection * purple_http_connection_new( + PurpleHttpRequest *request, PurpleConnection *gc); +static void purple_http_connection_terminate(PurpleHttpConnection *hc); +static void purple_http_conn_notify_progress_watcher(PurpleHttpConnection *hc); +static void +purple_http_conn_retry(PurpleHttpConnection *http_conn); + +static PurpleHttpResponse * purple_http_response_new(void); +static void purple_http_response_free(PurpleHttpResponse *response); + +static void purple_http_cookie_jar_parse(PurpleHttpCookieJar *cookie_jar, + GList *values); +static gchar * purple_http_cookie_jar_gen(PurpleHttpCookieJar *cookie_jar); +gchar * purple_http_cookie_jar_dump(PurpleHttpCookieJar *cjar); + +static PurpleHttpKeepaliveRequest * +purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, + PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, + PurpleHttpSocketConnectCb cb, gpointer user_data); +static void +purple_http_keepalive_pool_request_cancel(PurpleHttpKeepaliveRequest *req); +static void +purple_http_keepalive_pool_release(PurpleHttpSocket *hs, gboolean invalidate); + +static void +purple_http_connection_set_remove(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn); + +static GRegex *purple_http_re_url, *purple_http_re_url_host, + *purple_http_re_rfc1123; + +/* + * Values: pointers to running PurpleHttpConnection. + */ +static GList *purple_http_hc_list; + +/* + * Keys: pointers to PurpleConnection. + * Values: GList of pointers to running PurpleHttpConnection. + */ +static GHashTable *purple_http_hc_by_gc; + +/* + * Keys: pointers to PurpleConnection. + * Values: gboolean TRUE. + */ +static GHashTable *purple_http_cancelling_gc; + +/* + * Keys: pointers to PurpleHttpConnection. + * Values: pointers to links in purple_http_hc_list. + */ +static GHashTable *purple_http_hc_by_ptr; + +/*** Helper functions *********************************************************/ + +static time_t purple_http_rfc1123_to_time(const gchar *str) +{ + static const gchar *months[13] = { + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec", NULL + }; + GMatchInfo *match_info; + gchar *d_date, *d_month, *d_year, *d_time; + int month; + gchar *iso_date; + time_t t; + + g_return_val_if_fail(str != NULL, 0); + + g_regex_match(purple_http_re_rfc1123, str, 0, &match_info); + if (!g_match_info_matches(match_info)) { + g_match_info_free(match_info); + return 0; + } + + d_date = g_match_info_fetch(match_info, 1); + d_month = g_match_info_fetch(match_info, 2); + d_year = g_match_info_fetch(match_info, 3); + d_time = g_match_info_fetch(match_info, 4); + + g_match_info_free(match_info); + + month = 0; + while (months[month] != NULL) { + if (0 == g_ascii_strcasecmp(d_month, months[month])) + break; + month++; + } + month++; + + iso_date = g_strdup_printf("%s-%02d-%sT%s+00:00", + d_year, month, d_date, d_time); + + g_free(d_date); + g_free(d_month); + g_free(d_year); + g_free(d_time); + + if (month > 12) { + purple_debug_warning("http", "Invalid month: %s\n", d_month); + g_free(iso_date); + return 0; + } + + t = purple_str_to_time(iso_date, TRUE, NULL, NULL, NULL); + + g_free(iso_date); + + return t; +} + +/*** GZip streams *************************************************************/ + +static PurpleHttpGzStream * +purple_http_gz_new(gsize max_output, gboolean is_deflate) +{ + PurpleHttpGzStream *gzs = g_new0(PurpleHttpGzStream, 1); + GZlibCompressorFormat format; + + if (is_deflate) + format = G_ZLIB_COMPRESSOR_FORMAT_RAW; + else /* is gzip */ + format = G_ZLIB_COMPRESSOR_FORMAT_GZIP; + + gzs->decompressor = g_zlib_decompressor_new(format); + gzs->max_output = max_output; + + return gzs; +} + +static GString * +purple_http_gz_put(PurpleHttpGzStream *gzs, const gchar *buf, gsize len) +{ + const gchar *compressed_buff; + gsize compressed_len; + GString *ret; + + g_return_val_if_fail(gzs != NULL, NULL); + g_return_val_if_fail(buf != NULL, NULL); + + if (gzs->failed) + return NULL; + + if (gzs->pending) { + g_string_append_len(gzs->pending, buf, len); + compressed_buff = gzs->pending->str; + compressed_len = gzs->pending->len; + } else { + compressed_buff = buf; + compressed_len = len; + } + + ret = g_string_new(NULL); + while (compressed_len > 0) { + GConverterResult gzres; + gchar decompressed_buff[PURPLE_HTTP_GZ_BUFF_LEN]; + gsize decompressed_len = 0; + gsize bytes_read = 0; + GError *error = NULL; + + gzres = g_converter_convert(G_CONVERTER(gzs->decompressor), + compressed_buff, compressed_len, + decompressed_buff, sizeof(decompressed_buff), + G_CONVERTER_NO_FLAGS, + &bytes_read, + &decompressed_len, + &error); + + compressed_buff += bytes_read; + compressed_len -= bytes_read; + + if (gzres == G_CONVERTER_CONVERTED || G_CONVERTER_FINISHED) { + if (decompressed_len == 0) + break; + if (gzs->decompressed + decompressed_len >= + gzs->max_output) + { + purple_debug_warning("http", "Maximum amount of" + " decompressed data is reached\n"); + decompressed_len = gzs->max_output - + gzs->decompressed; + gzres = G_CONVERTER_FINISHED; + } + gzs->decompressed += decompressed_len; + g_string_append_len(ret, decompressed_buff, + decompressed_len); + if (gzres == G_CONVERTER_FINISHED) + break; + } else { + purple_debug_error("http", + "Decompression failed (%d): %s\n", gzres, + error->message); + g_clear_error(&error); + gzs->failed = TRUE; + return NULL; + } + } + + if (gzs->pending) { + g_string_free(gzs->pending, TRUE); + gzs->pending = NULL; + } + + if (compressed_len > 0) { + gzs->pending = g_string_new_len(compressed_buff, + compressed_len); + } + + return ret; +} + +static void +purple_http_gz_free(PurpleHttpGzStream *gzs) +{ + if (gzs == NULL) + return; + g_object_unref(gzs->decompressor); + if (gzs->pending) + g_string_free(gzs->pending, TRUE); + g_free(gzs); +} + +/*** NTLM *********************************************************************/ + +/** + * purple_ntlm_gen_type1: + * @hostname: Your hostname + * @domain: The domain to authenticate to + * + * Generates the base64 encoded type 1 message needed for NTLM authentication + * + * Returns: base64 encoded string to send to the server. This should + * be g_free'd by the caller. + */ +static gchar * +purple_http_ntlm_gen_type1(const gchar *hostname, const gchar *domain) +{ + int hostnamelen,host_off; + int domainlen,dom_off; + unsigned char *msg; + struct _ntlm_type1_message *tmsg; + gchar *tmp; + + hostnamelen = strlen(hostname); + domainlen = strlen(domain); + host_off = sizeof(struct _ntlm_type1_message); + dom_off = sizeof(struct _ntlm_type1_message) + hostnamelen; + msg = g_malloc0(sizeof(struct _ntlm_type1_message) + hostnamelen + domainlen); + tmsg = (struct _ntlm_type1_message*)(gpointer)msg; + tmsg->protocol[0] = 'N'; + tmsg->protocol[1] = 'T'; + tmsg->protocol[2] = 'L'; + tmsg->protocol[3] = 'M'; + tmsg->protocol[4] = 'S'; + tmsg->protocol[5] = 'S'; + tmsg->protocol[6] = 'P'; + tmsg->protocol[7] = '\0'; + tmsg->type = GUINT32_TO_LE(0x00000001); + tmsg->flags = GUINT32_TO_LE(0x0000b203); + tmsg->dom_len1 = tmsg->dom_len2 = GUINT16_TO_LE(domainlen); + tmsg->dom_off = GUINT32_TO_LE(dom_off); + tmsg->host_len1 = tmsg->host_len2 = GUINT16_TO_LE(hostnamelen); + tmsg->host_off = GUINT32_TO_LE(host_off); + memcpy(msg + host_off, hostname, hostnamelen); + memcpy(msg + dom_off, domain, domainlen); + + tmp = g_base64_encode(msg, sizeof(struct _ntlm_type1_message) + hostnamelen + domainlen); + g_free(msg); + + return tmp; +} + +/*** HTTP Sockets *************************************************************/ + +static gchar * +purple_http_socket_hash(const gchar *host, int port, gboolean is_ssl) +{ + return g_strdup_printf("%c:%s:%d", (is_ssl ? 'S' : 'R'), host, port); +} + +static void +purple_http_socket_connect_new_cb(GObject *source, GAsyncResult *res, + gpointer user_data) +{ + PurpleHttpSocket *hs = user_data; + GSocketConnection *conn; + PurpleHttpSocketConnectCb cb; + gpointer cb_data; + GError *error = NULL; + + conn = g_socket_client_connect_to_host_finish(G_SOCKET_CLIENT(source), + res, &error); + + cb = g_object_steal_data(source, "cb"); + cb_data = g_object_steal_data(source, "cb_data"); + + if (conn == NULL) { + if (!g_error_matches(error, + G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + cb(hs, error->message, cb_data); + } + + g_clear_error(&error); + return; + } + + hs->conn = conn; + + cb(hs, NULL, cb_data); +} + +static PurpleHttpSocket * +purple_http_socket_connect_new(PurpleConnection *gc, const gchar *host, + int port, gboolean is_ssl, + PurpleHttpSocketConnectCb cb, gpointer user_data) +{ + PurpleHttpSocket *hs; + GSocketClient *client; + GError *error = NULL; + + client = purple_gio_socket_client_new( + purple_connection_get_account(gc), &error); + + if (client == NULL) { + purple_debug_error("http", "Error connecting to '%s:%d': %s", + host, port, error->message); + g_clear_error(&error); + return NULL; + } + + hs = g_new0(PurpleHttpSocket, 1); + hs->cancellable = g_cancellable_new(); + + g_socket_client_set_tls(client, is_ssl); + g_object_set_data(G_OBJECT(client), "cb", cb); + g_object_set_data(G_OBJECT(client), "cb_data", user_data); + + g_socket_client_connect_to_host_async(client, + host, port, hs->cancellable, + purple_http_socket_connect_new_cb, hs); + + g_object_unref(client); + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "new socket created: %p\n", hs); + + return hs; +} + +static void +purple_http_socket_close_free(PurpleHttpSocket *hs) +{ + if (hs == NULL) + return; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "destroying socket: %p\n", hs); + + if (hs->input_source > 0) { + g_source_remove(hs->input_source); + hs->input_source = 0; + } + + if (hs->output_source > 0) { + g_source_remove(hs->output_source); + hs->output_source = 0; + } + + if (hs->cancellable != NULL) { + g_cancellable_cancel(hs->cancellable); + g_clear_object(&hs->cancellable); + } + + if (hs->conn != NULL) { + purple_gio_graceful_close(G_IO_STREAM(hs->conn), NULL, NULL); + g_clear_object(&hs->conn); + } + + g_free(hs); +} + +/*** Headers collection *******************************************************/ + +static PurpleHttpHeaders * purple_http_headers_new(void); +static void purple_http_headers_free(PurpleHttpHeaders *hdrs); +static void purple_http_headers_add(PurpleHttpHeaders *hdrs, const gchar *key, + const gchar *value); +static const GList * purple_http_headers_get_all(PurpleHttpHeaders *hdrs); +static GList * purple_http_headers_get_all_by_name( + PurpleHttpHeaders *hdrs, const gchar *key); +static const gchar * purple_http_headers_get(PurpleHttpHeaders *hdrs, + const gchar *key); +static gboolean purple_http_headers_get_int(PurpleHttpHeaders *hdrs, + const gchar *key, int *dst); +static gboolean purple_http_headers_match(PurpleHttpHeaders *hdrs, + const gchar *key, const gchar *value); +static gchar * purple_http_headers_dump(PurpleHttpHeaders *hdrs); + +static PurpleHttpHeaders * purple_http_headers_new(void) +{ + PurpleHttpHeaders *hdrs = g_new0(PurpleHttpHeaders, 1); + + hdrs->by_name = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)g_list_free); + + return hdrs; +} + +static void purple_http_headers_free_kvp(PurpleKeyValuePair *kvp) +{ + g_free(kvp->key); + g_free(kvp->value); + g_free(kvp); +} + +static void purple_http_headers_free(PurpleHttpHeaders *hdrs) +{ + if (hdrs == NULL) + return; + + g_hash_table_destroy(hdrs->by_name); + g_list_free_full(hdrs->list, + (GDestroyNotify)purple_http_headers_free_kvp); + g_free(hdrs); +} + +static void purple_http_headers_add(PurpleHttpHeaders *hdrs, const gchar *key, + const gchar *value) +{ + PurpleKeyValuePair *kvp; + GList *named_values, *new_values; + gchar *key_low; + + g_return_if_fail(hdrs != NULL); + g_return_if_fail(key != NULL); + g_return_if_fail(value != NULL); + + kvp = g_new0(PurpleKeyValuePair, 1); + kvp->key = g_strdup(key); + kvp->value = g_strdup(value); + hdrs->list = g_list_append(hdrs->list, kvp); + + key_low = g_ascii_strdown(key, -1); + named_values = g_hash_table_lookup(hdrs->by_name, key_low); + new_values = g_list_append(named_values, kvp->value); + if (named_values) + g_free(key_low); + else + g_hash_table_insert(hdrs->by_name, key_low, new_values); +} + +static void purple_http_headers_remove(PurpleHttpHeaders *hdrs, + const gchar *key) +{ + GList *it, *curr; + + g_return_if_fail(hdrs != NULL); + g_return_if_fail(key != NULL); + + if (!g_hash_table_remove(hdrs->by_name, key)) + return; + + /* Could be optimized to O(1). */ + it = g_list_first(hdrs->list); + while (it) { + PurpleKeyValuePair *kvp = it->data; + curr = it; + it = g_list_next(it); + if (g_ascii_strcasecmp(kvp->key, key) != 0) + continue; + + hdrs->list = g_list_delete_link(hdrs->list, curr); + purple_http_headers_free_kvp(kvp); + } +} + +static const GList * purple_http_headers_get_all(PurpleHttpHeaders *hdrs) +{ + g_return_val_if_fail(hdrs != NULL, NULL); + + return hdrs->list; +} + +/* return const */ +static GList * purple_http_headers_get_all_by_name( + PurpleHttpHeaders *hdrs, const gchar *key) +{ + GList *values; + gchar *key_low; + + g_return_val_if_fail(hdrs != NULL, NULL); + g_return_val_if_fail(key != NULL, NULL); + + key_low = g_ascii_strdown(key, -1); + values = g_hash_table_lookup(hdrs->by_name, key_low); + g_free(key_low); + + return values; +} + +static const gchar * purple_http_headers_get(PurpleHttpHeaders *hdrs, + const gchar *key) +{ + const GList *values = purple_http_headers_get_all_by_name(hdrs, key); + + if (!values) + return NULL; + + return values->data; +} + +static gboolean purple_http_headers_get_int(PurpleHttpHeaders *hdrs, + const gchar *key, int *dst) +{ + int val; + const gchar *str; + + str = purple_http_headers_get(hdrs, key); + if (!str) + return FALSE; + + if (1 != sscanf(str, "%d", &val)) + return FALSE; + + *dst = val; + return TRUE; +} + +static gboolean purple_http_headers_match(PurpleHttpHeaders *hdrs, + const gchar *key, const gchar *value) +{ + const gchar *str; + + str = purple_http_headers_get(hdrs, key); + if (str == NULL || value == NULL) + return str == value; + + return (g_ascii_strcasecmp(str, value) == 0); +} + +static gchar * purple_http_headers_dump(PurpleHttpHeaders *hdrs) +{ + const GList *hdr; + + GString *s = g_string_new(""); + + hdr = purple_http_headers_get_all(hdrs); + while (hdr) { + PurpleKeyValuePair *kvp = hdr->data; + hdr = g_list_next(hdr); + + g_string_append_printf(s, "%s: %s%s", kvp->key, + (gchar*)kvp->value, hdr ? "\n" : ""); + } + + return g_string_free(s, FALSE); +} + +/*** HTTP protocol backend ****************************************************/ + +static void _purple_http_disconnect(PurpleHttpConnection *hc, + gboolean is_graceful); + +static void _purple_http_gen_headers(PurpleHttpConnection *hc); +static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc); +static gboolean _purple_http_recv(GObject *source, gpointer _hc); +static gboolean _purple_http_send(GObject *source, gpointer _hc); + +/* closes current connection (if exists), estabilishes one and proceeds with + * request */ +static gboolean _purple_http_reconnect(PurpleHttpConnection *hc); + +static void _purple_http_error(PurpleHttpConnection *hc, const char *format, + ...) G_GNUC_PRINTF(2, 3); + +static void _purple_http_error(PurpleHttpConnection *hc, const char *format, + ...) +{ + va_list args; + + va_start(args, format); + hc->response->error = g_strdup_vprintf(format, args); + va_end(args); + + if (purple_debug_is_verbose()) + purple_debug_warning("http", "error: %s\n", hc->response->error); + + purple_http_conn_cancel(hc); +} + +static void _purple_http_gen_headers(PurpleHttpConnection *hc) +{ + GString *h; + PurpleHttpURL *url; + const GList *hdr; + PurpleHttpRequest *req; + PurpleHttpHeaders *hdrs; + gchar *request_url, *tmp_url = NULL; + + PurpleProxyInfo *proxy; + gboolean proxy_http = FALSE; + const gchar *proxy_username, *proxy_password; + + g_return_if_fail(hc != NULL); + + if (hc->request_header != NULL) + return; + + req = hc->request; + url = hc->url; + hdrs = req->headers; + proxy = purple_proxy_get_setup(hc->gc ? + purple_connection_get_account(hc->gc) : NULL); + + proxy_http = (purple_proxy_info_get_proxy_type(proxy) == PURPLE_PROXY_HTTP || + purple_proxy_info_get_proxy_type(proxy) == PURPLE_PROXY_USE_ENVVAR); + /* this is HTTP proxy, but used with tunelling with CONNECT */ + if (proxy_http && url->port != 80) + proxy_http = FALSE; + + hc->request_header = h = g_string_new(""); + hc->request_header_written = 0; + hc->request_contents_written = 0; + + if (proxy_http) + request_url = tmp_url = purple_http_url_print(url); + else + request_url = url->path; + + g_string_append_printf(h, "%s %s HTTP/%s\r\n", + req->method ? req->method : "GET", + request_url, + req->http11 ? "1.1" : "1.0"); + + g_free(tmp_url); + + if (!purple_http_headers_get(hdrs, "host")) + g_string_append_printf(h, "Host: %s\r\n", url->host); + if (!purple_http_headers_get(hdrs, "connection")) { + g_string_append(h, "Connection: "); + g_string_append(h, hc->is_keepalive ? + "Keep-Alive\r\n" : "close\r\n"); + } + if (!purple_http_headers_get(hdrs, "accept")) + g_string_append(h, "Accept: */*\r\n"); + if (!purple_http_headers_get(hdrs, "accept-encoding")) + g_string_append(h, "Accept-Encoding: gzip, deflate\r\n"); + + if (!purple_http_headers_get(hdrs, "content-length") && ( + req->contents_length > 0 || + purple_http_request_is_method(req, "post"))) + { + g_string_append_printf(h, "Content-Length: %u\r\n", + req->contents_length); + } + + if (proxy_http) + g_string_append(h, "Proxy-Connection: close\r\n"); /* TEST: proxy+KeepAlive */ + + proxy_username = purple_proxy_info_get_username(proxy); + if (proxy_http && proxy_username != NULL && proxy_username[0] != '\0') { + gchar *proxy_auth, *ntlm_type1, *tmp; + int len; + + proxy_password = purple_proxy_info_get_password(proxy); + if (proxy_password == NULL) + proxy_password = ""; + + tmp = g_strdup_printf("%s:%s", proxy_username, proxy_password); + len = strlen(tmp); + proxy_auth = g_base64_encode((const guchar *)tmp, len); + memset(tmp, 0, len); + g_free(tmp); + + ntlm_type1 = purple_http_ntlm_gen_type1(purple_get_host_name(), + ""); + + g_string_append_printf(h, "Proxy-Authorization: Basic %s\r\n", + proxy_auth); + g_string_append_printf(h, "Proxy-Authorization: NTLM %s\r\n", + ntlm_type1); + g_string_append(h, "Proxy-Connection: close\r\n"); /* TEST: proxy+KeepAlive */ + + memset(proxy_auth, 0, strlen(proxy_auth)); + g_free(proxy_auth); + g_free(ntlm_type1); + } + + hdr = purple_http_headers_get_all(hdrs); + while (hdr) { + PurpleKeyValuePair *kvp = hdr->data; + hdr = g_list_next(hdr); + + g_string_append_printf(h, "%s: %s\r\n", + kvp->key, (gchar*)kvp->value); + } + + if (!purple_http_cookie_jar_is_empty(req->cookie_jar)) { + gchar * cookies = purple_http_cookie_jar_gen(req->cookie_jar); + g_string_append_printf(h, "Cookie: %s\r\n", cookies); + g_free(cookies); + } + + g_string_append_printf(h, "\r\n"); + + if (purple_debug_is_unsafe() && purple_debug_is_verbose()) { + purple_debug_misc("http", "Generated request headers:\n%s", + h->str); + } +} + +static gboolean _purple_http_recv_headers(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + gchar *eol, *delim; + + if (hc->headers_got) { + purple_debug_error("http", "Headers already got\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + g_string_append_len(hc->response_buffer, buf, len); + if (hc->response_buffer->len > PURPLE_HTTP_MAX_RECV_BUFFER_LEN) { + purple_debug_error("http", + "Buffer too big when parsing headers\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + while ((eol = strstr(hc->response_buffer->str, "\r\n")) + != NULL) + { + gchar *hdrline = hc->response_buffer->str; + int hdrline_len = eol - hdrline; + + hdrline[hdrline_len] = '\0'; + + if (hdrline[0] == '\0') { + if (!hc->main_header_got) { + if (purple_debug_is_verbose() && + hc->is_keepalive) + { + purple_debug_misc("http", "Got keep-" + "alive terminator from previous" + " request\n"); + } else { + purple_debug_warning("http", "Got empty" + " line at the beginning - this " + "may be a HTTP server quirk\n"); + } + } else /* hc->main_header_got */ { + hc->headers_got = TRUE; + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "Got headers " + "end\n"); + } + } + } else if (!hc->main_header_got) { + hc->main_header_got = TRUE; + delim = strchr(hdrline, ' '); + if (delim == NULL || 1 != sscanf(delim + 1, "%d", + &hc->response->code)) + { + purple_debug_warning("http", + "Invalid response code\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + if (purple_debug_is_verbose()) + purple_debug_misc("http", + "Got main header with code %d\n", + hc->response->code); + } else { + if (purple_debug_is_verbose() && + purple_debug_is_unsafe()) + purple_debug_misc("http", "Got header: %s\n", + hdrline); + delim = strchr(hdrline, ':'); + if (delim == NULL || delim == hdrline) { + purple_debug_warning("http", + "Bad header delimiter\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + *delim++ = '\0'; + while (*delim == ' ') + delim++; + + purple_http_headers_add(hc->response->headers, hdrline, delim); + } + + g_string_erase(hc->response_buffer, 0, hdrline_len + 2); + if (hc->headers_got) + break; + } + return TRUE; +} + +static gboolean _purple_http_recv_body_data(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + GString *decompressed = NULL; + + if (hc->length_expected >= 0 && + len + hc->length_got > (guint)hc->length_expected) + { + len = hc->length_expected - hc->length_got; + } + + hc->length_got += len; + + if (hc->gz_stream != NULL) { + decompressed = purple_http_gz_put(hc->gz_stream, buf, len); + if (decompressed == NULL) { + _purple_http_error(hc, + _("Error while decompressing data")); + return FALSE; + } + buf = decompressed->str; + len = decompressed->len; + } + + g_assert(hc->request->max_length <= + PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH); + if (hc->length_got_decompressed + len > hc->request->max_length) { + purple_debug_warning("http", + "Maximum length exceeded, truncating\n"); + len = hc->request->max_length - hc->length_got_decompressed; + hc->length_expected = hc->length_got; + } + hc->length_got_decompressed += len; + + if (len == 0) { + if (decompressed != NULL) + g_string_free(decompressed, TRUE); + return TRUE; + } + + if (hc->request->response_writer != NULL) { + gboolean succ; + succ = hc->request->response_writer(hc, hc->response, buf, + hc->length_got_decompressed, len, + hc->request->response_writer_data); + if (!succ) { + if (decompressed != NULL) + g_string_free(decompressed, TRUE); + purple_debug_error("http", + "Cannot write using callback\n"); + _purple_http_error(hc, + _("Error handling retrieved data")); + return FALSE; + } + } else { + if (hc->response->contents == NULL) + hc->response->contents = g_string_new(""); + g_string_append_len(hc->response->contents, buf, len); + } + + if (decompressed != NULL) + g_string_free(decompressed, TRUE); + + purple_http_conn_notify_progress_watcher(hc); + return TRUE; +} + +static gboolean _purple_http_recv_body_chunked(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + gchar *eol, *line; + int line_len; + + if (hc->chunks_done) + return FALSE; + if (!hc->response_buffer) + hc->response_buffer = g_string_new(""); + + g_string_append_len(hc->response_buffer, buf, len); + if (hc->response_buffer->len > PURPLE_HTTP_MAX_RECV_BUFFER_LEN) { + purple_debug_error("http", + "Buffer too big when searching for chunk\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + while (hc->response_buffer->len > 0) { + if (hc->in_chunk) { + int got_now = hc->response_buffer->len; + if (hc->chunk_got + got_now > hc->chunk_length) + got_now = hc->chunk_length - hc->chunk_got; + hc->chunk_got += got_now; + + if (!_purple_http_recv_body_data(hc, + hc->response_buffer->str, got_now)) + return FALSE; + + g_string_erase(hc->response_buffer, 0, got_now); + hc->in_chunk = (hc->chunk_got < hc->chunk_length); + + continue; + } + + line = hc->response_buffer->str; + eol = strstr(line, "\r\n"); + if (eol == line) { + g_string_erase(hc->response_buffer, 0, 2); + line = hc->response_buffer->str; + eol = strstr(line, "\r\n"); + } + if (eol == NULL) { + /* waiting for more data (unlikely, but possible) */ + if (hc->response_buffer->len > 20) { + purple_debug_warning("http", "Chunk length not " + "found (buffer too large)\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + return TRUE; + } + line_len = eol - line; + + if (1 != sscanf(line, "%x", &hc->chunk_length)) { + if (purple_debug_is_unsafe()) + purple_debug_warning("http", + "Chunk length not found in [%s]\n", + line); + else + purple_debug_warning("http", + "Chunk length not found\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + hc->chunk_got = 0; + hc->in_chunk = TRUE; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "Found chunk of length %d\n", hc->chunk_length); + + g_string_erase(hc->response_buffer, 0, line_len + 2); + + if (hc->chunk_length == 0) { + hc->chunks_done = TRUE; + hc->in_chunk = FALSE; + return TRUE; + } + } + + return TRUE; +} + +static gboolean _purple_http_recv_body(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + if (hc->is_chunked) + return _purple_http_recv_body_chunked(hc, buf, len); + + return _purple_http_recv_body_data(hc, buf, len); +} + +static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc) +{ + int len; + gchar buf[4096]; + gboolean got_anything; + GError *error = NULL; + + len = g_pollable_input_stream_read_nonblocking( + G_POLLABLE_INPUT_STREAM( + g_io_stream_get_input_stream( + G_IO_STREAM(hc->socket->conn))), + buf, sizeof(buf), hc->socket->cancellable, + &error); + got_anything = (len > 0); + + if (len < 0 && (g_error_matches(error, + G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK) || + g_error_matches(error, + G_IO_ERROR, G_IO_ERROR_CANCELLED))) { + g_clear_error(&error); + return FALSE; + } + + if (len < 0) { + _purple_http_error(hc, _("Error reading from %s: %s"), + hc->url->host, error->message); + g_clear_error(&error); + return FALSE; + } + + /* EOF */ + if (len == 0) { + if (hc->request->max_length == 0) { + /* It's definitely YHttpServer quirk. */ + purple_debug_warning("http", "Got EOF, but no data was " + "expected (this may be a server quirk)\n"); + hc->length_expected = hc->length_got; + } + if (hc->length_expected >= 0 && + hc->length_got < (guint)hc->length_expected) + { + purple_debug_warning("http", "No more data while reading" + " contents\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + if (hc->headers_got) + hc->length_expected = hc->length_got; + else if (hc->length_got == 0 && hc->socket->use_count > 1) { + purple_debug_info("http", "Keep-alive connection " + "expired (when reading), retrying...\n"); + purple_http_conn_retry(hc); + return FALSE; + } else { + const gchar *server = purple_http_headers_get( + hc->response->headers, "Server"); + if (server && + g_ascii_strcasecmp(server, "YHttpServer") == 0) + { + purple_debug_warning("http", "No more data " + "while parsing headers (YHttpServer " + "quirk)\n"); + hc->headers_got = TRUE; + hc->length_expected = hc->length_got = 0; + hc->length_got_decompressed = 0; + } else { + purple_debug_warning("http", "No more data " + "while parsing headers\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + } + } + + if (!hc->headers_got && len > 0) { + if (!_purple_http_recv_headers(hc, buf, len)) + return FALSE; + len = 0; + if (hc->headers_got) { + gboolean is_gzip, is_deflate; + if (!purple_http_headers_get_int(hc->response->headers, + "Content-Length", &hc->length_expected)) + hc->length_expected = -1; + hc->is_chunked = (purple_http_headers_match( + hc->response->headers, + "Transfer-Encoding", "chunked")); + is_gzip = purple_http_headers_match( + hc->response->headers, "Content-Encoding", + "gzip"); + is_deflate = purple_http_headers_match( + hc->response->headers, "Content-Encoding", + "deflate"); + if (is_gzip || is_deflate) { + hc->gz_stream = purple_http_gz_new( + hc->request->max_length + 1, + is_deflate); + } + } + if (hc->headers_got && hc->response_buffer && + hc->response_buffer->len > 0) + { + int buffer_len = hc->response_buffer->len; + gchar *buffer = g_string_free(hc->response_buffer, FALSE); + hc->response_buffer = NULL; + _purple_http_recv_body(hc, buffer, buffer_len); + g_free(buffer); + } + if (!hc->headers_got) + return got_anything; + } + + if (len > 0) { + if (!_purple_http_recv_body(hc, buf, len)) + return FALSE; + } + + if (hc->is_chunked && hc->chunks_done && hc->length_expected < 0) + hc->length_expected = hc->length_got; + + if (hc->length_expected >= 0 && + hc->length_got >= (guint)hc->length_expected) + { + const gchar *redirect; + + if (hc->is_chunked && !hc->chunks_done) { + if (len == 0) { + _purple_http_error(hc, _("Chunked connection terminated")); + return FALSE; + } + if (purple_debug_is_verbose()) { + purple_debug_misc("http", + "I need the terminating empty chunk\n"); + } + return TRUE; + } + + if (!hc->headers_got) { + hc->response->code = 0; + purple_debug_warning("http", "No headers got\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + if (purple_debug_is_unsafe() && purple_debug_is_verbose()) { + gchar *hdrs = purple_http_headers_dump( + hc->response->headers); + purple_debug_misc("http", "Got response headers: %s\n", + hdrs); + g_free(hdrs); + } + + purple_http_cookie_jar_parse(hc->request->cookie_jar, + purple_http_headers_get_all_by_name( + hc->response->headers, "Set-Cookie")); + + if (purple_debug_is_unsafe() && purple_debug_is_verbose() && + !purple_http_cookie_jar_is_empty( + hc->request->cookie_jar)) + { + gchar *cookies = purple_http_cookie_jar_dump( + hc->request->cookie_jar); + purple_debug_misc("http", "Cookies: %s\n", cookies); + g_free(cookies); + } + + if (hc->response->code == 407) { + _purple_http_error(hc, _("Invalid proxy credentials")); + return FALSE; + } + + redirect = purple_http_headers_get(hc->response->headers, + "location"); + if (redirect && (hc->request->max_redirects == -1 || + hc->request->max_redirects > hc->redirects_count)) + { + PurpleHttpURL *url = purple_http_url_parse(redirect); + + hc->redirects_count++; + + if (!url) { + if (purple_debug_is_unsafe()) + purple_debug_warning("http", + "Invalid redirect to %s\n", + redirect); + else + purple_debug_warning("http", + "Invalid redirect\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + } + + purple_http_url_relative(hc->url, url); + purple_http_url_free(url); + + _purple_http_reconnect(hc); + return FALSE; + } + + _purple_http_disconnect(hc, TRUE); + purple_http_connection_terminate(hc); + return FALSE; + } + + return got_anything; +} + +static gboolean _purple_http_recv(GObject *source, gpointer _hc) +{ + PurpleHttpConnection *hc = _hc; + + while (_purple_http_recv_loopbody(hc)); + + return G_SOURCE_CONTINUE; +} + +static void _purple_http_send_got_data(PurpleHttpConnection *hc, + gboolean success, gboolean eof, size_t stored) +{ + int estimated_length; + + g_return_if_fail(hc != NULL); + + if (!success) { + _purple_http_error(hc, _("Error requesting data to write")); + return; + } + + hc->contents_reader_requested = FALSE; + g_string_set_size(hc->contents_reader_buffer, stored); + if (!eof) + return; + + estimated_length = hc->request_contents_written + stored; + + if (hc->request->contents_length != -1 && + hc->request->contents_length != estimated_length) + { + purple_debug_warning("http", + "Invalid amount of data has been written\n"); + } + hc->request->contents_length = estimated_length; +} + +static gboolean _purple_http_send(GObject *source, gpointer _hc) +{ + PurpleHttpConnection *hc = _hc; + int written, write_len; + const gchar *write_from; + gboolean writing_headers; + GError *error = NULL; + GSource *gsource; + + /* Waiting for data. This could be written more efficiently, by removing + * (and later, adding) hs->inpa. */ + if (hc->contents_reader_requested) + return G_SOURCE_CONTINUE; + + _purple_http_gen_headers(hc); + + writing_headers = + (hc->request_header_written < hc->request_header->len); + if (writing_headers) { + write_from = hc->request_header->str + + hc->request_header_written; + write_len = hc->request_header->len - + hc->request_header_written; + } else if (hc->request->contents_reader) { + if (hc->contents_reader_requested) + return G_SOURCE_CONTINUE; /* waiting for data */ + if (!hc->contents_reader_buffer) + hc->contents_reader_buffer = g_string_new(""); + if (hc->contents_reader_buffer->len == 0) { + hc->contents_reader_requested = TRUE; + g_string_set_size(hc->contents_reader_buffer, + PURPLE_HTTP_MAX_READ_BUFFER_LEN); + hc->request->contents_reader(hc, + hc->contents_reader_buffer->str, + hc->request_contents_written, + PURPLE_HTTP_MAX_READ_BUFFER_LEN, + hc->request->contents_reader_data, + _purple_http_send_got_data); + return G_SOURCE_CONTINUE; + } + write_from = hc->contents_reader_buffer->str; + write_len = hc->contents_reader_buffer->len; + } else { + write_from = hc->request->contents + + hc->request_contents_written; + write_len = hc->request->contents_length - + hc->request_contents_written; + } + + if (write_len == 0) { + purple_debug_warning("http", "Nothing to write\n"); + written = 0; + } else { + written = g_pollable_output_stream_write_nonblocking( + G_POLLABLE_OUTPUT_STREAM( + g_io_stream_get_output_stream( + G_IO_STREAM(hc->socket->conn))), + write_from, write_len, hc->socket->cancellable, + &error); + } + + if (written < 0 && (g_error_matches(error, + G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK) || + g_error_matches(error, + G_IO_ERROR, G_IO_ERROR_CANCELLED))) { + g_clear_error(&error); + return G_SOURCE_CONTINUE; + } + + if (written < 0) { + if (hc->request_header_written == 0 && + hc->socket->use_count > 1) + { + purple_debug_info("http", "Keep-alive connection " + "expired (when writing), retrying...\n"); + purple_http_conn_retry(hc); + } else { + _purple_http_error(hc, _("Error writing to %s: %s"), + hc->url->host, error->message); + } + + g_clear_error(&error); + return G_SOURCE_CONTINUE; + } + + if (writing_headers) { + hc->request_header_written += written; + purple_http_conn_notify_progress_watcher(hc); + if (hc->request_header_written < hc->request_header->len) + return G_SOURCE_CONTINUE; + if (hc->request->contents_length > 0) + return G_SOURCE_CONTINUE; + } else { + hc->request_contents_written += written; + purple_http_conn_notify_progress_watcher(hc); + if (hc->contents_reader_buffer) + g_string_erase(hc->contents_reader_buffer, 0, written); + if (hc->request->contents_length > 0 && + hc->request_contents_written < + (guint)hc->request->contents_length) + { + return G_SOURCE_CONTINUE; + } + } + + /* request is completely written, let's read the response */ + hc->is_reading = TRUE; + gsource = g_pollable_input_stream_create_source( + G_POLLABLE_INPUT_STREAM( + g_io_stream_get_input_stream( + G_IO_STREAM(hc->socket->conn))), + NULL); + g_source_set_callback(gsource, + (GSourceFunc)_purple_http_recv, hc, NULL); + hc->socket->input_source = g_source_attach(gsource, NULL); + g_source_unref(gsource); + + hc->socket->output_source = 0; + return G_SOURCE_REMOVE; +} + +static void _purple_http_disconnect(PurpleHttpConnection *hc, + gboolean is_graceful) +{ + g_return_if_fail(hc != NULL); + + if (hc->request_header) + g_string_free(hc->request_header, TRUE); + hc->request_header = NULL; + + if (hc->response_buffer) + g_string_free(hc->response_buffer, TRUE); + hc->response_buffer = NULL; + + if (hc->socket_request) + purple_http_keepalive_pool_request_cancel(hc->socket_request); + else { + purple_http_keepalive_pool_release(hc->socket, !is_graceful); + hc->socket = NULL; + } +} + +static void +_purple_http_connected(PurpleHttpSocket *hs, const gchar *error, gpointer _hc) +{ + PurpleHttpConnection *hc = _hc; + GSource *source; + + hc->socket_request = NULL; + hc->socket = hs; + + if (error != NULL) { + _purple_http_error(hc, _("Unable to connect to %s: %s"), + hc->url->host, error); + return; + } + + source = g_pollable_output_stream_create_source( + G_POLLABLE_OUTPUT_STREAM( + g_io_stream_get_output_stream(G_IO_STREAM(hs->conn))), + NULL); + g_source_set_callback(source, + (GSourceFunc)_purple_http_send, hc, NULL); + hc->socket->output_source = g_source_attach(source, NULL); + g_source_unref(source); +} + +static gboolean _purple_http_reconnect(PurpleHttpConnection *hc) +{ + PurpleHttpURL *url; + gboolean is_ssl = FALSE; + + g_return_val_if_fail(hc != NULL, FALSE); + g_return_val_if_fail(hc->url != NULL, FALSE); + + _purple_http_disconnect(hc, TRUE); + + if (purple_debug_is_verbose()) { + if (purple_debug_is_unsafe()) { + gchar *urlp = purple_http_url_print(hc->url); + purple_debug_misc("http", "Connecting to %s...\n", urlp); + g_free(urlp); + } else + purple_debug_misc("http", "Connecting to %s...\n", + hc->url->host); + } + + url = hc->url; + if (g_strcmp0(url->protocol, "") == 0 || + g_ascii_strcasecmp(url->protocol, "http") == 0) + { + /* do nothing */ + } else if (g_ascii_strcasecmp(url->protocol, "https") == 0) { + is_ssl = TRUE; + } else { + _purple_http_error(hc, _("Unsupported protocol: %s"), + url->protocol); + return FALSE; + } + + if (hc->request->keepalive_pool != NULL) { + hc->socket_request = purple_http_keepalive_pool_request( + hc->request->keepalive_pool, hc->gc, url->host, + url->port, is_ssl, _purple_http_connected, hc); + } else { + hc->socket = purple_http_socket_connect_new(hc->gc, url->host, + url->port, is_ssl, _purple_http_connected, hc); + } + + if (hc->socket_request == NULL && hc->socket == NULL) { + _purple_http_error(hc, _("Unable to connect to %s"), url->host); + return FALSE; + } + + purple_http_headers_free(hc->response->headers); + hc->response->headers = purple_http_headers_new(); + hc->response_buffer = g_string_new(""); + hc->main_header_got = FALSE; + hc->headers_got = FALSE; + if (hc->response->contents != NULL) + g_string_free(hc->response->contents, TRUE); + hc->response->contents = NULL; + hc->length_got = 0; + hc->length_got_decompressed = 0; + hc->length_expected = -1; + hc->is_chunked = FALSE; + hc->in_chunk = FALSE; + hc->chunks_done = FALSE; + + purple_http_conn_notify_progress_watcher(hc); + + return TRUE; +} + +/*** Performing HTTP requests *************************************************/ + +static gboolean purple_http_request_timeout(gpointer _hc) +{ + PurpleHttpConnection *hc = _hc; + + purple_debug_warning("http", "Timeout reached for request %p\n", hc); + + purple_http_conn_cancel(hc); + + return FALSE; +} + +PurpleHttpConnection * purple_http_get(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, const gchar *url) +{ + PurpleHttpRequest *request; + PurpleHttpConnection *hc; + + g_return_val_if_fail(url != NULL, NULL); + + request = purple_http_request_new(url); + hc = purple_http_request(gc, request, callback, user_data); + purple_http_request_unref(request); + + return hc; +} + +PurpleHttpConnection * purple_http_get_printf(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, + const gchar *format, ...) +{ + va_list args; + gchar *value; + PurpleHttpConnection *ret; + + g_return_val_if_fail(format != NULL, NULL); + + va_start(args, format); + value = g_strdup_vprintf(format, args); + va_end(args); + + ret = purple_http_get(gc, callback, user_data, value); + g_free(value); + + return ret; +} + +PurpleHttpConnection * purple_http_request(PurpleConnection *gc, + PurpleHttpRequest *request, PurpleHttpCallback callback, + gpointer user_data) +{ + PurpleHttpConnection *hc; + + g_return_val_if_fail(request != NULL, NULL); + + if (request->url == NULL) { + purple_debug_error("http", "Cannot perform new request - " + "URL is not set\n"); + return NULL; + } + + if (g_hash_table_lookup(purple_http_cancelling_gc, gc)) { + purple_debug_warning("http", "Cannot perform another HTTP " + "request while cancelling all related with this " + "PurpleConnection\n"); + return NULL; + } + + hc = purple_http_connection_new(request, gc); + hc->callback = callback; + hc->user_data = user_data; + + hc->url = purple_http_url_parse(request->url); + + if (purple_debug_is_unsafe()) + purple_debug_misc("http", "Performing new request %p for %s.\n", + hc, request->url); + else + purple_debug_misc("http", "Performing new request %p to %s.\n", + hc, hc->url ? hc->url->host : NULL); + + if (!hc->url || hc->url->host == NULL || hc->url->host[0] == '\0') { + purple_debug_error("http", "Invalid URL requested.\n"); + purple_http_connection_terminate(hc); + return NULL; + } + + _purple_http_reconnect(hc); + + hc->timeout_handle = g_timeout_add_seconds(request->timeout, + purple_http_request_timeout, hc); + + return hc; +} + +/*** HTTP connection API ******************************************************/ + +static void purple_http_connection_free(PurpleHttpConnection *hc); +static gboolean purple_http_conn_notify_progress_watcher_timeout(gpointer _hc); + +static PurpleHttpConnection * purple_http_connection_new( + PurpleHttpRequest *request, PurpleConnection *gc) +{ + PurpleHttpConnection *hc = g_new0(PurpleHttpConnection, 1); + + g_assert(request != NULL); + + hc->request = request; + purple_http_request_ref(request); + hc->response = purple_http_response_new(); + hc->is_keepalive = (request->keepalive_pool != NULL); + + hc->link_global = purple_http_hc_list = + g_list_prepend(purple_http_hc_list, hc); + g_hash_table_insert(purple_http_hc_by_ptr, hc, hc->link_global); + if (gc) { + GList *gc_list = g_hash_table_lookup(purple_http_hc_by_gc, gc); + g_hash_table_steal(purple_http_hc_by_gc, gc); + hc->link_gc = gc_list = g_list_prepend(gc_list, hc); + g_hash_table_insert(purple_http_hc_by_gc, gc, gc_list); + hc->gc = gc; + } + + return hc; +} + +static void purple_http_connection_free(PurpleHttpConnection *hc) +{ + if (hc->timeout_handle) + g_source_remove(hc->timeout_handle); + if (hc->watcher_delayed_handle) + g_source_remove(hc->watcher_delayed_handle); + + if (hc->connection_set != NULL) + purple_http_connection_set_remove(hc->connection_set, hc); + + purple_http_url_free(hc->url); + purple_http_request_unref(hc->request); + purple_http_response_free(hc->response); + + if (hc->contents_reader_buffer) + g_string_free(hc->contents_reader_buffer, TRUE); + purple_http_gz_free(hc->gz_stream); + + if (hc->request_header) + g_string_free(hc->request_header, TRUE); + + purple_http_hc_list = g_list_delete_link(purple_http_hc_list, + hc->link_global); + g_hash_table_remove(purple_http_hc_by_ptr, hc); + if (hc->gc) { + GList *gc_list, *gc_list_new; + gc_list = g_hash_table_lookup(purple_http_hc_by_gc, hc->gc); + g_assert(gc_list != NULL); + + gc_list_new = g_list_delete_link(gc_list, hc->link_gc); + if (gc_list != gc_list_new) { + g_hash_table_steal(purple_http_hc_by_gc, hc->gc); + if (gc_list_new) + g_hash_table_insert(purple_http_hc_by_gc, + hc->gc, gc_list_new); + } + } + + g_free(hc); +} + +/* call callback and do the cleanup */ +static void purple_http_connection_terminate(PurpleHttpConnection *hc) +{ + g_return_if_fail(hc != NULL); + + purple_debug_misc("http", "Request %p performed %s.\n", hc, + purple_http_response_is_successful(hc->response) ? + "successfully" : "without success"); + + if (hc->callback) + hc->callback(hc, hc->response, hc->user_data); + + purple_http_connection_free(hc); +} + +void purple_http_conn_cancel(PurpleHttpConnection *http_conn) +{ + if (http_conn == NULL) + return; + + if (http_conn->is_cancelling) + return; + http_conn->is_cancelling = TRUE; + + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "Cancelling connection %p...\n", + http_conn); + } + + http_conn->response->code = 0; + _purple_http_disconnect(http_conn, FALSE); + purple_http_connection_terminate(http_conn); +} + +static void +purple_http_conn_retry(PurpleHttpConnection *http_conn) +{ + if (http_conn == NULL) + return; + + purple_debug_info("http", "Retrying connection %p...\n", http_conn); + + http_conn->response->code = 0; + _purple_http_disconnect(http_conn, FALSE); + _purple_http_reconnect(http_conn); +} + +void purple_http_conn_cancel_all(PurpleConnection *gc) +{ + GList *gc_list; + + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "Cancelling all running HTTP " + "connections\n"); + } + + gc_list = g_hash_table_lookup(purple_http_hc_by_gc, gc); + + g_hash_table_insert(purple_http_cancelling_gc, gc, GINT_TO_POINTER(TRUE)); + + while (gc_list) { + PurpleHttpConnection *hc = gc_list->data; + gc_list = g_list_next(gc_list); + purple_http_conn_cancel(hc); + } + + g_hash_table_remove(purple_http_cancelling_gc, gc); + + if (NULL != g_hash_table_lookup(purple_http_hc_by_gc, gc)) + purple_debug_fatal("http", "Couldn't cancel all connections " + "related to gc=%p (it shouldn't happen)\n", gc); +} + +gboolean purple_http_conn_is_running(PurpleHttpConnection *http_conn) +{ + if (http_conn == NULL) + return FALSE; + return (NULL != g_hash_table_lookup(purple_http_hc_by_ptr, http_conn)); +} + +PurpleHttpRequest * purple_http_conn_get_request(PurpleHttpConnection *http_conn) +{ + g_return_val_if_fail(http_conn != NULL, NULL); + + return http_conn->request; +} + +PurpleHttpCookieJar * purple_http_conn_get_cookie_jar( + PurpleHttpConnection *http_conn) +{ + return purple_http_request_get_cookie_jar(purple_http_conn_get_request( + http_conn)); +} + +PurpleConnection * purple_http_conn_get_purple_connection( + PurpleHttpConnection *http_conn) +{ + g_return_val_if_fail(http_conn != NULL, NULL); + + return http_conn->gc; +} + +void purple_http_conn_set_progress_watcher(PurpleHttpConnection *http_conn, + PurpleHttpProgressWatcher watcher, gpointer user_data, + gint interval_threshold) +{ + g_return_if_fail(http_conn != NULL); + + if (interval_threshold < 0) { + interval_threshold = + PURPLE_HTTP_PROGRESS_WATCHER_DEFAULT_INTERVAL; + } + + http_conn->watcher = watcher; + http_conn->watcher_user_data = user_data; + http_conn->watcher_interval_threshold = interval_threshold; +} + +static void purple_http_conn_notify_progress_watcher( + PurpleHttpConnection *hc) +{ + gint64 now; + gboolean reading_state; + int processed, total; + + g_return_if_fail(hc != NULL); + + if (hc->watcher == NULL) + return; + + reading_state = hc->is_reading; + if (reading_state) { + total = hc->length_expected; + processed = hc->length_got; + } else { + total = hc->request->contents_length; + processed = hc->request_contents_written; + if (total == 0) + total = -1; + } + if (total != -1 && total < processed) { + purple_debug_warning("http", "Processed too much\n"); + total = processed; + } + + now = g_get_monotonic_time(); + if (hc->watcher_last_call + hc->watcher_interval_threshold + > now && processed != total) + { + if (hc->watcher_delayed_handle) + return; + hc->watcher_delayed_handle = g_timeout_add_seconds( + 1 + hc->watcher_interval_threshold / 1000000, + purple_http_conn_notify_progress_watcher_timeout, hc); + return; + } + + if (hc->watcher_delayed_handle) + g_source_remove(hc->watcher_delayed_handle); + hc->watcher_delayed_handle = 0; + + hc->watcher_last_call = now; + hc->watcher(hc, reading_state, processed, total, hc->watcher_user_data); +} + +static gboolean purple_http_conn_notify_progress_watcher_timeout(gpointer _hc) +{ + PurpleHttpConnection *hc = _hc; + + purple_http_conn_notify_progress_watcher(hc); + + return FALSE; +} + +/*** Cookie jar API ***********************************************************/ + +static PurpleHttpCookie * purple_http_cookie_new(const gchar *value); +void purple_http_cookie_free(PurpleHttpCookie *cookie); + +static void purple_http_cookie_jar_set_ext(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value, time_t expires); + +static PurpleHttpCookie * purple_http_cookie_new(const gchar *value) +{ + PurpleHttpCookie *cookie = g_new0(PurpleHttpCookie, 1); + + cookie->value = g_strdup(value); + cookie->expires = -1; + + return cookie; +} + +void purple_http_cookie_free(PurpleHttpCookie *cookie) +{ + g_free(cookie->value); + g_free(cookie); +} + +void purple_http_cookie_jar_free(PurpleHttpCookieJar *cookie_jar); + +PurpleHttpCookieJar * purple_http_cookie_jar_new(void) +{ + PurpleHttpCookieJar *cjar = g_new0(PurpleHttpCookieJar, 1); + + cjar->ref_count = 1; + cjar->tab = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)purple_http_cookie_free); + + return cjar; +} + +void purple_http_cookie_jar_free(PurpleHttpCookieJar *cookie_jar) +{ + g_hash_table_destroy(cookie_jar->tab); + g_free(cookie_jar); +} + +void purple_http_cookie_jar_ref(PurpleHttpCookieJar *cookie_jar) +{ + g_return_if_fail(cookie_jar != NULL); + + cookie_jar->ref_count++; +} + +PurpleHttpCookieJar * purple_http_cookie_jar_unref( + PurpleHttpCookieJar *cookie_jar) +{ + if (cookie_jar == NULL) + return NULL; + + g_return_val_if_fail(cookie_jar->ref_count > 0, NULL); + + cookie_jar->ref_count--; + if (cookie_jar->ref_count > 0) + return cookie_jar; + + purple_http_cookie_jar_free(cookie_jar); + return NULL; +} + +static void purple_http_cookie_jar_parse(PurpleHttpCookieJar *cookie_jar, + GList *values) +{ + values = g_list_first(values); + while (values) { + const gchar *cookie = values->data; + const gchar *eqsign, *semicolon; + gchar *name, *value; + time_t expires = -1; + values = g_list_next(values); + + eqsign = strchr(cookie, '='); + semicolon = strchr(cookie, ';'); + + if (eqsign == NULL || eqsign == cookie || + (semicolon != NULL && semicolon < eqsign)) + { + if (purple_debug_is_unsafe()) + purple_debug_warning("http", + "Invalid cookie: [%s]\n", cookie); + else + purple_debug_warning("http", "Invalid cookie."); + continue; + } + + name = g_strndup(cookie, eqsign - cookie); + eqsign++; + if (semicolon != NULL) + value = g_strndup(eqsign, semicolon - eqsign); + else + value = g_strdup(eqsign); + + if (semicolon != NULL) { + GMatchInfo *match_info; + GRegex *re_expires = g_regex_new( /* XXX: make it static */ + "expires=([a-z0-9, :]+)", + G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + g_regex_match(re_expires, semicolon, 0, &match_info); + if (g_match_info_matches(match_info)) { + gchar *expire_date = + g_match_info_fetch(match_info, 1); + expires = purple_http_rfc1123_to_time( + expire_date); + g_free(expire_date); + } + g_match_info_free(match_info); + + g_regex_unref(re_expires); + } + + purple_http_cookie_jar_set_ext(cookie_jar, name, value, expires); + + g_free(name); + g_free(value); + } +} + +static gchar * purple_http_cookie_jar_gen(PurpleHttpCookieJar *cookie_jar) +{ + GHashTableIter it; + gchar *key; + PurpleHttpCookie *cookie; + GString *str; + time_t now = time(NULL); + + g_return_val_if_fail(cookie_jar != NULL, NULL); + + str = g_string_new(""); + + g_hash_table_iter_init(&it, cookie_jar->tab); + while (g_hash_table_iter_next(&it, (gpointer*)&key, + (gpointer*)&cookie)) + { + if (cookie->expires != -1 && cookie->expires != 0 && cookie->expires <= now) + continue; + g_string_append_printf(str, "%s=%s; ", key, cookie->value); + } + + if (str->len > 0) + g_string_truncate(str, str->len - 2); + return g_string_free(str, FALSE); +} + +void purple_http_cookie_jar_set(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value) +{ + gchar *escaped_name = g_strdup(purple_url_encode(name)); + gchar *escaped_value = NULL; + + if (value) { + escaped_value = g_strdup(purple_url_encode(value)); + } + + purple_http_cookie_jar_set_ext(cookie_jar, escaped_name, escaped_value, -1); + + g_free(escaped_name); + g_free(escaped_value); +} + +static void purple_http_cookie_jar_set_ext(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value, time_t expires) +{ + g_return_if_fail(cookie_jar != NULL); + g_return_if_fail(name != NULL); + + if (expires != -1 && expires != 0 && time(NULL) >= expires) + value = NULL; + + if (value != NULL) { + PurpleHttpCookie *cookie = purple_http_cookie_new(value); + cookie->expires = expires; + g_hash_table_insert(cookie_jar->tab, g_strdup(name), cookie); + } else + g_hash_table_remove(cookie_jar->tab, name); +} + +gchar * purple_http_cookie_jar_get(PurpleHttpCookieJar *cookie_jar, + const gchar *name) +{ + PurpleHttpCookie *cookie; + + g_return_val_if_fail(cookie_jar != NULL, NULL); + g_return_val_if_fail(name != NULL, NULL); + + cookie = g_hash_table_lookup(cookie_jar->tab, name); + if (!cookie) + return NULL; + + return g_strdup(purple_url_decode(cookie->value)); +} + +gchar * purple_http_cookie_jar_dump(PurpleHttpCookieJar *cjar) +{ + GHashTableIter it; + gchar *key; + PurpleHttpCookie *cookie; + GString *str = g_string_new(""); + + g_hash_table_iter_init(&it, cjar->tab); + while (g_hash_table_iter_next(&it, (gpointer*)&key, (gpointer*)&cookie)) + g_string_append_printf(str, "%s: %s (expires: %" G_GINT64_FORMAT + ")\n", key, cookie->value, (gint64)cookie->expires); + + if (str->len > 0) + g_string_truncate(str, str->len - 1); + return g_string_free(str, FALSE); +} + +gboolean purple_http_cookie_jar_is_empty(PurpleHttpCookieJar *cookie_jar) +{ + g_return_val_if_fail(cookie_jar != NULL, TRUE); + + return g_hash_table_size(cookie_jar->tab) == 0; +} + +/*** HTTP Keep-Alive pool API *************************************************/ + +static void +purple_http_keepalive_host_process_queue(PurpleHttpKeepaliveHost *host); + +static void +purple_http_keepalive_host_free(gpointer _host) +{ + PurpleHttpKeepaliveHost *host = _host; + + g_free(host->host); + + g_slist_free_full(host->queue, + (GDestroyNotify)purple_http_keepalive_pool_request_cancel); + g_slist_free_full(host->sockets, + (GDestroyNotify)purple_http_socket_close_free); + + if (host->process_queue_timeout > 0) { + g_source_remove(host->process_queue_timeout); + host->process_queue_timeout = 0; + } + + + g_free(host); +} + +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_new(void) +{ + PurpleHttpKeepalivePool *pool = g_new0(PurpleHttpKeepalivePool, 1); + + pool->ref_count = 1; + pool->by_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + purple_http_keepalive_host_free); + + return pool; +} + +static void +purple_http_keepalive_pool_free(PurpleHttpKeepalivePool *pool) +{ + g_return_if_fail(pool != NULL); + + if (pool->is_destroying) + return; + pool->is_destroying = TRUE; + g_hash_table_destroy(pool->by_hash); + g_free(pool); +} + +void +purple_http_keepalive_pool_ref(PurpleHttpKeepalivePool *pool) +{ + g_return_if_fail(pool != NULL); + + pool->ref_count++; +} + +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_unref(PurpleHttpKeepalivePool *pool) +{ + if (pool == NULL) + return NULL; + + g_return_val_if_fail(pool->ref_count > 0, NULL); + + pool->ref_count--; + if (pool->ref_count > 0) + return pool; + + purple_http_keepalive_pool_free(pool); + return NULL; +} + +static PurpleHttpKeepaliveRequest * +purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, + PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, + PurpleHttpSocketConnectCb cb, gpointer user_data) +{ + PurpleHttpKeepaliveRequest *req; + PurpleHttpKeepaliveHost *kahost; + gchar *hash; + + g_return_val_if_fail(pool != NULL, NULL); + g_return_val_if_fail(host != NULL, NULL); + + if (pool->is_destroying) { + purple_debug_error("http", "pool is destroying\n"); + return NULL; + } + + hash = purple_http_socket_hash(host, port, is_ssl); + kahost = g_hash_table_lookup(pool->by_hash, hash); + + if (kahost == NULL) { + kahost = g_new0(PurpleHttpKeepaliveHost, 1); + kahost->pool = pool; + kahost->host = g_strdup(host); + kahost->port = port; + kahost->is_ssl = is_ssl; + + g_hash_table_insert(pool->by_hash, g_strdup(hash), kahost); + } + + g_free(hash); + + req = g_new0(PurpleHttpKeepaliveRequest, 1); + req->gc = gc; + req->cb = cb; + req->user_data = user_data; + req->host = kahost; + + kahost->queue = g_slist_append(kahost->queue, req); + + purple_http_keepalive_host_process_queue(kahost); + + return req; +} + +static void +_purple_http_keepalive_socket_connected(PurpleHttpSocket *hs, + const gchar *error, gpointer _req) +{ + PurpleHttpKeepaliveRequest *req = _req; + + if (hs != NULL) + hs->use_count++; + + req->cb(hs, error, req->user_data); + g_free(req); +} + +static gboolean +_purple_http_keepalive_host_process_queue_cb(gpointer _host) +{ + PurpleHttpKeepaliveRequest *req; + PurpleHttpKeepaliveHost *host = _host; + PurpleHttpSocket *hs = NULL; + GSList *it; + guint sockets_count; + + g_return_val_if_fail(host != NULL, FALSE); + + host->process_queue_timeout = 0; + + if (host->queue == NULL) + return FALSE; + + sockets_count = 0; + it = host->sockets; + while (it != NULL) { + PurpleHttpSocket *hs_current = it->data; + + sockets_count++; + + if (!hs_current->is_busy) { + hs = hs_current; + break; + } + + it = g_slist_next(it); + } + + /* There are no free sockets and we cannot create another one. */ + if (hs == NULL && sockets_count >= host->pool->limit_per_host && + host->pool->limit_per_host > 0) + { + return FALSE; + } + + req = host->queue->data; + host->queue = g_slist_remove(host->queue, req); + + if (hs != NULL) { + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "locking a (previously used) " + "socket: %p\n", hs); + } + + hs->is_busy = TRUE; + hs->use_count++; + + purple_http_keepalive_host_process_queue(host); + + req->cb(hs, NULL, req->user_data); + g_free(req); + + return FALSE; + } + + hs = purple_http_socket_connect_new(req->gc, req->host->host, + req->host->port, req->host->is_ssl, + _purple_http_keepalive_socket_connected, req); + if (hs == NULL) { + purple_debug_error("http", "failed creating new socket"); + return FALSE; + } + + req->hs = hs; + hs->is_busy = TRUE; + hs->host = host; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "locking a (new) socket: %p\n", hs); + + host->sockets = g_slist_append(host->sockets, hs); + + return FALSE; +} + +static void +purple_http_keepalive_host_process_queue(PurpleHttpKeepaliveHost *host) +{ + g_return_if_fail(host != NULL); + + if (host->process_queue_timeout > 0) + return; + + host->process_queue_timeout = g_timeout_add(0, + _purple_http_keepalive_host_process_queue_cb, host); +} + +static void +purple_http_keepalive_pool_request_cancel(PurpleHttpKeepaliveRequest *req) +{ + if (req == NULL) + return; + + if (req->host != NULL) + req->host->queue = g_slist_remove(req->host->queue, req); + + if (req->hs != NULL) { + if (G_LIKELY(req->host)) { + req->host->sockets = g_slist_remove(req->host->sockets, + req->hs); + } + purple_http_socket_close_free(req->hs); + /* req should already be free'd here */ + } else { + req->cb(NULL, _("Cancelled"), req->user_data); + g_free(req); + } +} + +static void +purple_http_keepalive_pool_release(PurpleHttpSocket *hs, gboolean invalidate) +{ + PurpleHttpKeepaliveHost *host; + + if (hs == NULL) + return; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "releasing a socket: %p\n", hs); + + if (hs->input_source > 0) { + g_source_remove(hs->input_source); + hs->input_source = 0; + } + + if (hs->output_source > 0) { + g_source_remove(hs->output_source); + hs->output_source = 0; + } + + hs->is_busy = FALSE; + host = hs->host; + + if (host == NULL) { + purple_http_socket_close_free(hs); + return; + } + + if (invalidate) { + host->sockets = g_slist_remove(host->sockets, hs); + purple_http_socket_close_free(hs); + } + + purple_http_keepalive_host_process_queue(host); +} + +void +purple_http_keepalive_pool_set_limit_per_host(PurpleHttpKeepalivePool *pool, + guint limit) +{ + g_return_if_fail(pool != NULL); + + pool->limit_per_host = limit; +} + +guint +purple_http_keepalive_pool_get_limit_per_host(PurpleHttpKeepalivePool *pool) +{ + g_return_val_if_fail(pool != NULL, 0); + + return pool->limit_per_host; +} + +/*** HTTP connection set API **************************************************/ + +PurpleHttpConnectionSet * +purple_http_connection_set_new(void) +{ + PurpleHttpConnectionSet *set; + + set = g_new0(PurpleHttpConnectionSet, 1); + set->connections = g_hash_table_new(g_direct_hash, g_direct_equal); + + return set; +} + +void +purple_http_connection_set_destroy(PurpleHttpConnectionSet *set) +{ + if (set == NULL) + return; + + set->is_destroying = TRUE; + + while (TRUE) { + GHashTableIter iter; + PurpleHttpConnection *http_conn; + + g_hash_table_iter_init(&iter, set->connections); + if (!g_hash_table_iter_next(&iter, (gpointer*)&http_conn, NULL)) + break; + + purple_http_conn_cancel(http_conn); + } + + g_hash_table_destroy(set->connections); + g_free(set); +} + +void +purple_http_connection_set_add(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn) +{ + if (set->is_destroying) + return; + if (http_conn->connection_set == set) + return; + if (http_conn->connection_set != NULL) { + purple_http_connection_set_remove(http_conn->connection_set, + http_conn); + } + g_hash_table_insert(set->connections, http_conn, (gpointer)TRUE); + http_conn->connection_set = set; +} + +static void +purple_http_connection_set_remove(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn) +{ + g_hash_table_remove(set->connections, http_conn); + if (http_conn->connection_set == set) + http_conn->connection_set = NULL; +} + +/*** Request API **************************************************************/ + +static void purple_http_request_free(PurpleHttpRequest *request); + +PurpleHttpRequest * purple_http_request_new(const gchar *url) +{ + PurpleHttpRequest *request; + + request = g_new0(PurpleHttpRequest, 1); + + request->ref_count = 1; + request->url = g_strdup(url); + request->headers = purple_http_headers_new(); + request->cookie_jar = purple_http_cookie_jar_new(); + request->keepalive_pool = purple_http_keepalive_pool_new(); + + request->timeout = PURPLE_HTTP_REQUEST_DEFAULT_TIMEOUT; + request->max_redirects = PURPLE_HTTP_REQUEST_DEFAULT_MAX_REDIRECTS; + request->http11 = TRUE; + request->max_length = PURPLE_HTTP_REQUEST_DEFAULT_MAX_LENGTH; + + return request; +} + +static void purple_http_request_free(PurpleHttpRequest *request) +{ + purple_http_headers_free(request->headers); + purple_http_cookie_jar_unref(request->cookie_jar); + purple_http_keepalive_pool_unref(request->keepalive_pool); + g_free(request->method); + g_free(request->contents); + g_free(request->url); + g_free(request); +} + +void purple_http_request_ref(PurpleHttpRequest *request) +{ + g_return_if_fail(request != NULL); + + request->ref_count++; +} + +PurpleHttpRequest * purple_http_request_unref(PurpleHttpRequest *request) +{ + if (request == NULL) + return NULL; + + g_return_val_if_fail(request->ref_count > 0, NULL); + + request->ref_count--; + if (request->ref_count > 0) + return request; + + purple_http_request_free(request); + return NULL; +} + +void purple_http_request_set_url(PurpleHttpRequest *request, const gchar *url) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(url != NULL); + + g_free(request->url); + request->url = g_strdup(url); +} + +void purple_http_request_set_url_printf(PurpleHttpRequest *request, + const gchar *format, ...) +{ + va_list args; + gchar *value; + + g_return_if_fail(request != NULL); + g_return_if_fail(format != NULL); + + va_start(args, format); + value = g_strdup_vprintf(format, args); + va_end(args); + + purple_http_request_set_url(request, value); + g_free(value); +} + +const gchar * purple_http_request_get_url(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, NULL); + + return request->url; +} + +void purple_http_request_set_method(PurpleHttpRequest *request, const gchar *method) +{ + g_return_if_fail(request != NULL); + + g_free(request->method); + request->method = g_strdup(method); +} + +const gchar * purple_http_request_get_method(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, NULL); + + return request->method; +} + +static gboolean purple_http_request_is_method(PurpleHttpRequest *request, + const gchar *method) +{ + const gchar *rmethod; + + g_return_val_if_fail(request != NULL, FALSE); + g_return_val_if_fail(method != NULL, FALSE); + + rmethod = purple_http_request_get_method(request); + if (rmethod == NULL) + return (g_ascii_strcasecmp(method, "get") == 0); + return (g_ascii_strcasecmp(method, rmethod) == 0); +} + +void +purple_http_request_set_keepalive_pool(PurpleHttpRequest *request, + PurpleHttpKeepalivePool *pool) +{ + g_return_if_fail(request != NULL); + + if (pool != NULL) + purple_http_keepalive_pool_ref(pool); + + if (request->keepalive_pool != NULL) { + purple_http_keepalive_pool_unref(request->keepalive_pool); + request->keepalive_pool = NULL; + } + + if (pool != NULL) + request->keepalive_pool = pool; +} + +PurpleHttpKeepalivePool * +purple_http_request_get_keepalive_pool(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, FALSE); + + return request->keepalive_pool; +} + +void purple_http_request_set_contents(PurpleHttpRequest *request, + const gchar *contents, int length) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(length >= -1); + + request->contents_reader = NULL; + request->contents_reader_data = NULL; + + g_free(request->contents); + if (contents == NULL || length == 0) { + request->contents = NULL; + request->contents_length = 0; + return; + } + + if (length == -1) + length = strlen(contents); + request->contents = g_memdup(contents, length); + request->contents_length = length; +} + +void purple_http_request_set_contents_reader(PurpleHttpRequest *request, + PurpleHttpContentReader reader, int contents_length, gpointer user_data) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(reader != NULL); + g_return_if_fail(contents_length >= -1); + + g_free(request->contents); + request->contents = NULL; + request->contents_length = contents_length; + request->contents_reader = reader; + request->contents_reader_data = user_data; +} + +void purple_http_request_set_response_writer(PurpleHttpRequest *request, + PurpleHttpContentWriter writer, gpointer user_data) +{ + g_return_if_fail(request != NULL); + + if (writer == NULL) + user_data = NULL; + request->response_writer = writer; + request->response_writer_data = user_data; +} + +void purple_http_request_set_timeout(PurpleHttpRequest *request, int timeout) +{ + g_return_if_fail(request != NULL); + + if (timeout < -1) + timeout = -1; + + request->timeout = timeout; +} + +int purple_http_request_get_timeout(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, -1); + + return request->timeout; +} + +void purple_http_request_set_max_redirects(PurpleHttpRequest *request, + int max_redirects) +{ + g_return_if_fail(request != NULL); + + if (max_redirects < -1) + max_redirects = -1; + + request->max_redirects = max_redirects; +} + +int purple_http_request_get_max_redirects(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, -1); + + return request->max_redirects; +} + +void purple_http_request_set_cookie_jar(PurpleHttpRequest *request, + PurpleHttpCookieJar *cookie_jar) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(cookie_jar != NULL); + + purple_http_cookie_jar_ref(cookie_jar); + purple_http_cookie_jar_unref(request->cookie_jar); + request->cookie_jar = cookie_jar; +} + +PurpleHttpCookieJar * purple_http_request_get_cookie_jar( + PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, NULL); + + return request->cookie_jar; +} + +void purple_http_request_set_http11(PurpleHttpRequest *request, gboolean http11) +{ + g_return_if_fail(request != NULL); + + request->http11 = http11; +} + +gboolean purple_http_request_is_http11(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, FALSE); + + return request->http11; +} + +void purple_http_request_set_max_len(PurpleHttpRequest *request, int max_len) +{ + g_return_if_fail(request != NULL); + + if (max_len < 0 || max_len > PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH) + max_len = PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH; + + request->max_length = max_len; +} + +int purple_http_request_get_max_len(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, -1); + + return request->max_length; +} + +void purple_http_request_header_set(PurpleHttpRequest *request, + const gchar *key, const gchar *value) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(key != NULL); + + purple_http_headers_remove(request->headers, key); + if (value) + purple_http_headers_add(request->headers, key, value); +} + +void purple_http_request_header_set_printf(PurpleHttpRequest *request, + const gchar *key, const gchar *format, ...) +{ + va_list args; + gchar *value; + + g_return_if_fail(request != NULL); + g_return_if_fail(key != NULL); + g_return_if_fail(format != NULL); + + va_start(args, format); + value = g_strdup_vprintf(format, args); + va_end(args); + + purple_http_request_header_set(request, key, value); + g_free(value); +} + +void purple_http_request_header_add(PurpleHttpRequest *request, + const gchar *key, const gchar *value) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(key != NULL); + + purple_http_headers_add(request->headers, key, value); +} + +/*** HTTP response API ********************************************************/ + +static PurpleHttpResponse * purple_http_response_new(void) +{ + PurpleHttpResponse *response = g_new0(PurpleHttpResponse, 1); + + return response; +} + +static void purple_http_response_free(PurpleHttpResponse *response) +{ + if (response->contents != NULL) + g_string_free(response->contents, TRUE); + g_free(response->error); + purple_http_headers_free(response->headers); + g_free(response); +} + +gboolean purple_http_response_is_successful(PurpleHttpResponse *response) +{ + int code; + + g_return_val_if_fail(response != NULL, FALSE); + + code = response->code; + + if (code <= 0) + return FALSE; + + /* TODO: HTTP/1.1 100 Continue */ + + if (code / 100 == 2) + return TRUE; + + return FALSE; +} + +int purple_http_response_get_code(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, 0); + + return response->code; +} + +const gchar * purple_http_response_get_error(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, NULL); + + if (response->error != NULL) + return response->error; + + if (!purple_http_response_is_successful(response)) { + static gchar errmsg[200]; + if (response->code <= 0) { + g_snprintf(errmsg, sizeof(errmsg), + _("Unknown HTTP error")); + } else { + g_snprintf(errmsg, sizeof(errmsg), + _("Invalid HTTP response code (%d)"), + response->code); + } + return errmsg; + } + + return NULL; +} + +gsize purple_http_response_get_data_len(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, 0); + + if (response->contents == NULL) + return 0; + + return response->contents->len; +} + +const gchar * purple_http_response_get_data(PurpleHttpResponse *response, size_t *len) +{ + const gchar *ret = ""; + + g_return_val_if_fail(response != NULL, ""); + + if (response->contents != NULL) { + ret = response->contents->str; + if (len) + *len = response->contents->len; + } else { + if (len) + *len = 0; + } + + return ret; +} + +const GList * purple_http_response_get_all_headers(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, NULL); + + return purple_http_headers_get_all(response->headers); +} + +const GList * purple_http_response_get_headers_by_name( + PurpleHttpResponse *response, const gchar *name) +{ + g_return_val_if_fail(response != NULL, NULL); + g_return_val_if_fail(name != NULL, NULL); + + return purple_http_headers_get_all_by_name(response->headers, name); +} + +const gchar * purple_http_response_get_header(PurpleHttpResponse *response, + const gchar *name) +{ + g_return_val_if_fail(response != NULL, NULL); + g_return_val_if_fail(name != NULL, NULL); + + return purple_http_headers_get(response->headers, name); +} + +/*** URL functions ************************************************************/ + +PurpleHttpURL * +purple_http_url_parse(const char *raw_url) +{ + PurpleHttpURL *url; + GMatchInfo *match_info; + + gchar *host_full, *tmp; + + g_return_val_if_fail(raw_url != NULL, NULL); + + if (!g_regex_match(purple_http_re_url, raw_url, 0, &match_info)) { + if (purple_debug_is_verbose() && purple_debug_is_unsafe()) { + purple_debug_warning("http", + "Invalid URL provided: %s\n", + raw_url); + } + return NULL; + } + + url = g_new0(PurpleHttpURL, 1); + + url->protocol = g_match_info_fetch(match_info, 1); + host_full = g_match_info_fetch(match_info, 2); + url->path = g_match_info_fetch(match_info, 3); + url->fragment = g_match_info_fetch(match_info, 4); + g_match_info_free(match_info); + + if (g_strcmp0(url->protocol, "") == 0) { + g_free(url->protocol); + url->protocol = NULL; + } else if (url->protocol != NULL) { + tmp = url->protocol; + url->protocol = g_ascii_strdown(url->protocol, -1); + g_free(tmp); + } + if (host_full[0] == '\0') { + g_free(host_full); + host_full = NULL; + } + if (url->path[0] == '\0') { + g_free(url->path); + url->path = NULL; + } + if ((url->protocol == NULL) != (host_full == NULL)) + purple_debug_warning("http", "Protocol or host not present " + "(unlikely case)\n"); + + if (host_full) { + gchar *port_str; + + if (!g_regex_match(purple_http_re_url_host, host_full, 0, + &match_info)) + { + if (purple_debug_is_verbose() && + purple_debug_is_unsafe()) + { + purple_debug_warning("http", + "Invalid host provided for URL: %s\n", + raw_url); + } + + g_free(host_full); + purple_http_url_free(url); + return NULL; + } + + url->username = g_match_info_fetch(match_info, 1); + url->password = g_match_info_fetch(match_info, 2); + url->host = g_match_info_fetch(match_info, 3); + port_str = g_match_info_fetch(match_info, 4); + + if (port_str && port_str[0]) + url->port = atoi(port_str); + + if (url->username[0] == '\0') { + g_free(url->username); + url->username = NULL; + } + if (url->password[0] == '\0') { + g_free(url->password); + url->password = NULL; + } + if (g_strcmp0(url->host, "") == 0) { + g_free(url->host); + url->host = NULL; + } else if (url->host != NULL) { + tmp = url->host; + url->host = g_ascii_strdown(url->host, -1); + g_free(tmp); + } + + g_free(port_str); + g_match_info_free(match_info); + + g_free(host_full); + host_full = NULL; + } + + if (url->host != NULL) { + if (url->protocol == NULL) + url->protocol = g_strdup("http"); + if (url->port == 0 && 0 == strcmp(url->protocol, "http")) + url->port = 80; + if (url->port == 0 && 0 == strcmp(url->protocol, "https")) + url->port = 443; + if (url->path == NULL) + url->path = g_strdup("/"); + if (url->path[0] != '/') + purple_debug_warning("http", + "URL path doesn't start with slash\n"); + } + + return url; +} + +void +purple_http_url_free(PurpleHttpURL *parsed_url) +{ + if (parsed_url == NULL) + return; + + g_free(parsed_url->protocol); + g_free(parsed_url->username); + g_free(parsed_url->password); + g_free(parsed_url->host); + g_free(parsed_url->path); + g_free(parsed_url->fragment); + g_free(parsed_url); +} + +void +purple_http_url_relative(PurpleHttpURL *base_url, PurpleHttpURL *relative_url) +{ + g_return_if_fail(base_url != NULL); + g_return_if_fail(relative_url != NULL); + + if (relative_url->host) { + g_free(base_url->protocol); + base_url->protocol = g_strdup(relative_url->protocol); + g_free(base_url->username); + base_url->username = g_strdup(relative_url->username); + g_free(base_url->password); + base_url->password = g_strdup(relative_url->password); + g_free(base_url->host); + base_url->host = g_strdup(relative_url->host); + base_url->port = relative_url->port; + + g_free(base_url->path); + base_url->path = NULL; + } + + if (relative_url->path) { + if (relative_url->path[0] == '/' || + base_url->path == NULL) + { + g_free(base_url->path); + base_url->path = g_strdup(relative_url->path); + } else { + gchar *last_slash = strrchr(base_url->path, '/'); + gchar *tmp; + if (last_slash == NULL) + base_url->path[0] = '\0'; + else + last_slash[1] = '\0'; + tmp = base_url->path; + base_url->path = g_strconcat(base_url->path, + relative_url->path, NULL); + g_free(tmp); + } + } + + g_free(base_url->fragment); + base_url->fragment = g_strdup(relative_url->fragment); +} + +gchar * +purple_http_url_print(PurpleHttpURL *parsed_url) +{ + GString *url = g_string_new(""); + gboolean before_host_printed = FALSE, host_printed = FALSE; + gboolean port_is_default = FALSE; + + if (parsed_url->protocol) { + g_string_append_printf(url, "%s://", parsed_url->protocol); + before_host_printed = TRUE; + if (parsed_url->port == 80 && 0 == strcmp(parsed_url->protocol, + "http")) + port_is_default = TRUE; + if (parsed_url->port == 443 && 0 == strcmp(parsed_url->protocol, + "https")) + port_is_default = TRUE; + } + if (parsed_url->username || parsed_url->password) { + if (parsed_url->username) + g_string_append(url, parsed_url->username); + g_string_append_printf(url, ":%s", parsed_url->password); + g_string_append(url, "@"); + before_host_printed = TRUE; + } + if (parsed_url->host || parsed_url->port) { + if (!parsed_url->host) + g_string_append_printf(url, "{???}:%d", + parsed_url->port); + else { + g_string_append(url, parsed_url->host); + if (!port_is_default) + g_string_append_printf(url, ":%d", + parsed_url->port); + } + host_printed = TRUE; + } + if (parsed_url->path) { + if (!host_printed && before_host_printed) + g_string_append(url, "{???}"); + g_string_append(url, parsed_url->path); + } + if (parsed_url->fragment) + g_string_append_printf(url, "#%s", parsed_url->fragment); + + return g_string_free(url, FALSE); +} + +const gchar * +purple_http_url_get_protocol(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->protocol; +} + +const gchar * +purple_http_url_get_username(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->username; +} + +const gchar * +purple_http_url_get_password(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->password; +} + +const gchar * +purple_http_url_get_host(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->host; +} + +int +purple_http_url_get_port(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, 0); + + return parsed_url->port; +} + +const gchar * +purple_http_url_get_path(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->path; +} + +const gchar * +purple_http_url_get_fragment(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->fragment; +} + +/*** HTTP Subsystem ***********************************************************/ + +void purple_http_init(void) +{ + purple_http_re_url = g_regex_new("^" + + "(?:" /* host part beginning */ + "([a-z]+)\\:/*" /* protocol */ + "([^/]+)" /* username, password, host, port */ + ")?" /* host part ending */ + + "([^#]*)" /* path */ + + "(?:#" "(.*)" ")?" /* fragment */ + + "$", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + purple_http_re_url_host = g_regex_new("^" + + "(?:" /* user credentials part beginning */ + "([" PURPLE_HTTP_URL_CREDENTIALS_CHARS "]+)" /* username */ + "(?::([" PURPLE_HTTP_URL_CREDENTIALS_CHARS "]+))" /* password */ + "@)?" /* user credentials part ending */ + + "([a-z0-9.-]+)" /* host */ + "(?::([0-9]+))?" /* port*/ + + "$", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + purple_http_re_rfc1123 = g_regex_new( + "^[a-z]+, " /* weekday */ + "([0-9]+) " /* date */ + "([a-z]+) " /* month */ + "([0-9]+) " /* year */ + "([0-9]+:[0-9]+:[0-9]+) " /* time */ + "(?:GMT|UTC)$", + G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + purple_http_hc_list = NULL; + purple_http_hc_by_ptr = g_hash_table_new(g_direct_hash, g_direct_equal); + purple_http_hc_by_gc = g_hash_table_new_full(g_direct_hash, + g_direct_equal, NULL, (GDestroyNotify)g_list_free); + purple_http_cancelling_gc = g_hash_table_new(g_direct_hash, g_direct_equal); +} + +static void purple_http_foreach_conn_cancel(gpointer _hc, gpointer user_data) +{ + PurpleHttpConnection *hc = _hc; + purple_http_conn_cancel(hc); +} + +void purple_http_uninit(void) +{ + g_regex_unref(purple_http_re_url); + purple_http_re_url = NULL; + g_regex_unref(purple_http_re_url_host); + purple_http_re_url_host = NULL; + g_regex_unref(purple_http_re_rfc1123); + purple_http_re_rfc1123 = NULL; + + g_list_foreach(purple_http_hc_list, purple_http_foreach_conn_cancel, + NULL); + + if (purple_http_hc_list != NULL || + 0 != g_hash_table_size(purple_http_hc_by_ptr) || + 0 != g_hash_table_size(purple_http_hc_by_gc)) + purple_debug_warning("http", + "Couldn't cleanup all connections.\n"); + + g_list_free(purple_http_hc_list); + purple_http_hc_list = NULL; + g_hash_table_destroy(purple_http_hc_by_gc); + purple_http_hc_by_gc = NULL; + g_hash_table_destroy(purple_http_hc_by_ptr); + purple_http_hc_by_ptr = NULL; + g_hash_table_destroy(purple_http_cancelling_gc); + purple_http_cancelling_gc = NULL; +} diff --git a/pidgin/libpurple/http.h b/pidgin/libpurple/http.h new file mode 100644 index 0000000..870d766 --- /dev/null +++ b/pidgin/libpurple/http.h @@ -0,0 +1,964 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _PURPLE_HTTP_H_ +#define _PURPLE_HTTP_H_ +/** + * SECTION:http + * @section_id: libpurple-http + * @short_description: <filename>http.h</filename> + * @title: HTTP API + */ + +#include <glib.h> + +#include "connection.h" + +/** + * PurpleHttpRequest: + * + * A structure containing all data required to generate a single HTTP request. + */ +typedef struct _PurpleHttpRequest PurpleHttpRequest; + +/** + * PurpleHttpConnection: + * + * A representation of actually running HTTP request. Can be used to cancel the + * request. + */ +typedef struct _PurpleHttpConnection PurpleHttpConnection; + +/** + * PurpleHttpResponse: + * + * All information got with response for HTTP request. + */ +typedef struct _PurpleHttpResponse PurpleHttpResponse; + +/** + * PurpleHttpURL: + * + * Parsed representation for the URL. + */ +typedef struct _PurpleHttpURL PurpleHttpURL; + +/** + * PurpleHttpCookieJar: + * + * An collection of cookies, got from HTTP response or provided for HTTP + * request. + */ +typedef struct _PurpleHttpCookieJar PurpleHttpCookieJar; + +/** + * PurpleHttpKeepalivePool: + * + * A pool of TCP connections for HTTP Keep-Alive session. + */ +typedef struct _PurpleHttpKeepalivePool PurpleHttpKeepalivePool; + +/** + * PurpleHttpConnectionSet: + * + * A set of running HTTP requests. Can be used to cancel all of them at once. + */ +typedef struct _PurpleHttpConnectionSet PurpleHttpConnectionSet; + +/** + * PurpleHttpCallback: + * + * An callback called after performing (successfully or not) HTTP request. + */ +typedef void (*PurpleHttpCallback)(PurpleHttpConnection *http_conn, + PurpleHttpResponse *response, gpointer user_data); + +/** + * PurpleHttpContentReaderCb: + * + * An callback called after storing data requested by PurpleHttpContentReader. + */ +typedef void (*PurpleHttpContentReaderCb)(PurpleHttpConnection *http_conn, + gboolean success, gboolean eof, size_t stored); + +/** + * PurpleHttpContentReader: + * @http_conn: Connection, which requests data. + * @buffer: Buffer to store data to (with offset ignored). + * @offset: Position, from where to read data. + * @length: Length of data to read. + * @user_data: The user data passed with callback function. + * @cb: The function to call after storing data to buffer. + * + * An callback for getting large request contents (ie. from file stored on + * disk). + */ +typedef void (*PurpleHttpContentReader)(PurpleHttpConnection *http_conn, + gchar *buffer, size_t offset, size_t length, gpointer user_data, + PurpleHttpContentReaderCb cb); + +/** + * PurpleHttpContentWriter: + * @http_conn: Connection, which requests data. + * @response: Response at point got so far (may change later). + * @buffer: Buffer to read data from (with offset ignored). + * @offset: Position of data got (its value is offset + length of + * previous call), can be safely ignored. + * @length: Length of data read. + * @user_data: The user data passed with callback function. + * + * An callback for writting large response contents. + * + * Returns: TRUE, if succeeded, FALSE otherwise. + */ +typedef gboolean (*PurpleHttpContentWriter)(PurpleHttpConnection *http_conn, + PurpleHttpResponse *response, const gchar *buffer, size_t offset, + size_t length, gpointer user_data); + +/** + * PurpleHttpProgressWatcher: + * @http_conn: The HTTP Connection. + * @reading_state: FALSE, is we are sending the request, TRUE, when reading + * the response. + * @processed: The amount of data already processed. + * @total: Total amount of data (in current state). + * @user_data: The user data passed with callback function. + * + * An callback for watching HTTP connection progress. + */ +typedef void (*PurpleHttpProgressWatcher)(PurpleHttpConnection *http_conn, + gboolean reading_state, int processed, int total, gpointer user_data); + +G_BEGIN_DECLS + +/**************************************************************************/ +/* Performing HTTP requests */ +/**************************************************************************/ + +/** + * purple_http_get: + * @gc: The connection for which the request is needed, or NULL. + * @callback: (scope call): The callback function. + * @user_data: The user data to pass to the callback function. + * @url: The URL. + * + * Fetches the data from a URL with GET request, and passes it to a callback + * function. + * + * Returns: The HTTP connection struct. + */ +PurpleHttpConnection * purple_http_get(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, const gchar *url); + +/** + * purple_http_get_printf: + * @gc: The connection for which the request is needed, or NULL. + * @callback: (scope call): The callback function. + * @user_data: The user data to pass to the callback function. + * @format: The format string. + * @...: The parameters to insert into the format string. + * + * Constructs an URL and fetches the data from it with GET request, then passes + * it to a callback function. + * + * Returns: The HTTP connection struct. + */ +PurpleHttpConnection * purple_http_get_printf(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, + const gchar *format, ...) G_GNUC_PRINTF(4, 5); + +/** + * purple_http_request: + * @gc: The connection for which the request is needed, or NULL. + * @request: The request. + * @callback: (scope call): The callback function. + * @user_data: The user data to pass to the callback function. + * + * Fetches a HTTP request and passes the response to a callback function. + * Provided request struct can be shared by multiple http requests but can not + * be modified when any of these is running. + * + * Returns: The HTTP connection struct. + */ +PurpleHttpConnection * purple_http_request(PurpleConnection *gc, + PurpleHttpRequest *request, PurpleHttpCallback callback, + gpointer user_data); + +/**************************************************************************/ +/* HTTP connection API */ +/**************************************************************************/ + +/** + * purple_http_conn_cancel: + * @http_conn: The data returned when you initiated the HTTP request. + * + * Cancel a pending HTTP request. + */ +void purple_http_conn_cancel(PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_cancel_all: + * @gc: The handle. + * + * Cancels all HTTP connections associated with the specified handle. + */ +void purple_http_conn_cancel_all(PurpleConnection *gc); + +/** + * purple_http_conn_is_running: + * @http_conn: The HTTP connection (may be invalid pointer). + * + * Checks, if provided HTTP request is running. + * + * Returns: TRUE, if provided connection is currently running. + */ +gboolean purple_http_conn_is_running(PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_get_request: + * @http_conn: The HTTP connection. + * + * Gets PurpleHttpRequest used for specified HTTP connection. + * + * Returns: The PurpleHttpRequest object. + */ +PurpleHttpRequest * purple_http_conn_get_request( + PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_get_cookie_jar: + * @http_conn: The HTTP connection. + * + * Gets cookie jar used within connection. + * + * Returns: The cookie jar. + */ +PurpleHttpCookieJar * purple_http_conn_get_cookie_jar( + PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_get_purple_connection: + * @http_conn: The HTTP connection. + * + * Gets PurpleConnection tied with specified HTTP connection. + * + * Returns: The PurpleConnection object. + */ +PurpleConnection * purple_http_conn_get_purple_connection( + PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_set_progress_watcher: + * @http_conn: The HTTP connection. + * @watcher: (scope call): The watcher. + * @user_data: The user data to pass to the callback function. + * @interval_threshold: Minimum interval (in microseconds) of calls to + * watcher, or -1 for default. + * + * Sets the watcher, called after writing or reading data to/from HTTP stream. + * May be used for updating transfer progress gauge. + */ +void purple_http_conn_set_progress_watcher(PurpleHttpConnection *http_conn, + PurpleHttpProgressWatcher watcher, gpointer user_data, + gint interval_threshold); + + +/**************************************************************************/ +/* URL processing API */ +/**************************************************************************/ + +/** + * purple_http_url_parse: + * @url: The URL to parse. + * + * Parses a URL. + * + * The returned data must be freed with purple_http_url_free. + * + * Returns: The parsed url or NULL, if the URL is invalid. + */ +PurpleHttpURL * +purple_http_url_parse(const char *url); + +/** + * purple_http_url_free: + * @parsed_url: The parsed URL struct, or NULL. + * + * Frees the parsed URL struct. + */ +void +purple_http_url_free(PurpleHttpURL *parsed_url); + +/** + * purple_http_url_relative: + * @base_url: The base URL. The result is stored here. + * @relative_url: The relative URL. + * + * Converts the base URL to the absolute form of the provided relative URL. + * + * Example: "https://example.com/path/to/file.html" + "subdir/other-file.html" = + * "https://example.com/path/to/subdir/another-file.html" + */ +void +purple_http_url_relative(PurpleHttpURL *base_url, PurpleHttpURL *relative_url); + +/** + * purple_http_url_print: + * @parsed_url: The URL struct. + * + * Converts the URL struct to the printable form. The result may not be a valid + * URL (in cases, when the struct doesn't have all fields filled properly). + * + * The result must be g_free'd. + * + * Returns: The printable form of the URL. + */ +gchar * +purple_http_url_print(PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_protocol: + * @parsed_url: The URL struct. + * + * Gets the protocol part of URL. + * + * Returns: The protocol. + */ +const gchar * +purple_http_url_get_protocol(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_username: + * @parsed_url: The URL struct. + * + * Gets the username part of URL. + * + * Returns: The username. + */ +const gchar * +purple_http_url_get_username(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_password: + * @parsed_url: The URL struct. + * + * Gets the password part of URL. + * + * Returns: The password. + */ +const gchar * +purple_http_url_get_password(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_host: + * @parsed_url: The URL struct. + * + * Gets the hostname part of URL. + * + * Returns: The hostname. + */ +const gchar * +purple_http_url_get_host(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_port: + * @parsed_url: The URL struct. + * + * Gets the port part of URL. + * + * Returns: The port number. + */ +int +purple_http_url_get_port(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_path: + * @parsed_url: The URL struct. + * + * Gets the path part of URL. + * + * Returns: The path. + */ +const gchar * +purple_http_url_get_path(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_fragment: + * @parsed_url: The URL struct. + * + * Gets the fragment part of URL. + * + * Returns: The fragment. + */ +const gchar * +purple_http_url_get_fragment(const PurpleHttpURL *parsed_url); + + +/**************************************************************************/ +/* Cookie jar API */ +/**************************************************************************/ + +/** + * purple_http_cookie_jar_new: + * + * Creates new cookie jar, + * + * Returns: empty cookie jar. + */ +PurpleHttpCookieJar * purple_http_cookie_jar_new(void); + +/** + * purple_http_cookie_jar_ref: + * @cookie_jar: The cookie jar. + * + * Increment the reference count. + */ +void purple_http_cookie_jar_ref(PurpleHttpCookieJar *cookie_jar); + +/** + * purple_http_cookie_jar_unref: + * @cookie_jar: The cookie jar. + * + * Decrement the reference count. + * + * If the reference count reaches zero, the cookie jar will be freed. + * + * Returns: @cookie_jar or %NULL if the reference count reached zero. + */ +PurpleHttpCookieJar * purple_http_cookie_jar_unref( + PurpleHttpCookieJar *cookie_jar); + +/** + * purple_http_cookie_jar_set: + * @cookie_jar: The cookie jar. + * @name: Cookie name. + * @value: Cookie contents. + * + * Sets the cookie. + */ +void purple_http_cookie_jar_set(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value); + +/** + * purple_http_cookie_jar_get: + * @cookie_jar: The cookie jar. + * @name: Cookie name. + * + * Gets the cookie. + * + * The result must be g_free'd. + * + * Returns: Cookie contents, or NULL, if cookie doesn't exists. + */ +gchar * purple_http_cookie_jar_get(PurpleHttpCookieJar *cookie_jar, + const gchar *name); + +/** + * purple_http_cookie_jar_is_empty: + * @cookie_jar: The cookie jar. + * + * Checks, if the cookie jar contains any cookies. + * + * Returns: TRUE, if cookie jar contains any cookie, FALSE otherwise. + */ +gboolean purple_http_cookie_jar_is_empty(PurpleHttpCookieJar *cookie_jar); + + +/**************************************************************************/ +/* HTTP Request API */ +/**************************************************************************/ + +/** + * purple_http_request_new: + * @url: The URL to request for, or NULL to leave empty (to be set with + * purple_http_request_set_url). + * + * Creates the new instance of HTTP request configuration. + * + * Returns: The new instance of HTTP request struct. + */ +PurpleHttpRequest * purple_http_request_new(const gchar *url); + +/** + * purple_http_request_ref: + * @request: The request. + * + * Increment the reference count. + */ +void purple_http_request_ref(PurpleHttpRequest *request); + +/** + * purple_http_request_unref: + * @request: The request. + * + * Decrement the reference count. + * + * If the reference count reaches zero, the http request struct will be freed. + * + * Returns: @request or %NULL if the reference count reached zero. + */ +PurpleHttpRequest * purple_http_request_unref(PurpleHttpRequest *request); + +/** + * purple_http_request_set_url: + * @request: The request. + * @url: The url. + * + * Sets URL for HTTP request. + */ +void purple_http_request_set_url(PurpleHttpRequest *request, const gchar *url); + +/** + * purple_http_request_set_url_printf: + * @request: The request. + * @format: The format string. + * @...: The parameters to insert into the format string. + * + * Constructs and sets an URL for HTTP request. + */ +void purple_http_request_set_url_printf(PurpleHttpRequest *request, + const gchar *format, ...) G_GNUC_PRINTF(2, 3); + +/** + * purple_http_request_get_url: + * @request: The request. + * + * Gets URL set for the HTTP request. + * + * Returns: URL set for this request. + */ +const gchar * purple_http_request_get_url(PurpleHttpRequest *request); + +/** + * purple_http_request_set_method: + * @request: The request. + * @method: The method, or NULL for default. + * + * Sets custom HTTP method used for the request. + */ +void purple_http_request_set_method(PurpleHttpRequest *request, + const gchar *method); + +/** + * purple_http_request_get_method: + * @request: The request. + * + * Gets HTTP method set for the request. + * + * Returns: The method. + */ +const gchar * purple_http_request_get_method(PurpleHttpRequest *request); + +/** + * purple_http_request_set_keepalive_pool: + * @request: The request. + * @pool: The new KeepAlive pool, or NULL to reset. + * + * Sets HTTP KeepAlive connections pool for the request. + * + * It increases pool's reference count. + */ +void +purple_http_request_set_keepalive_pool(PurpleHttpRequest *request, + PurpleHttpKeepalivePool *pool); + +/** + * purple_http_request_get_keepalive_pool: + * @request: The request. + * + * Gets HTTP KeepAlive connections pool associated with the request. + * + * It doesn't affect pool's reference count. + * + * Returns: The KeepAlive pool, used for the request. + */ +PurpleHttpKeepalivePool * +purple_http_request_get_keepalive_pool(PurpleHttpRequest *request); + +/** + * purple_http_request_set_contents: + * @request: The request. + * @contents: The contents. + * @length: The length of contents (-1 if it's a NULL-terminated string) + * + * Sets contents of HTTP request (for example, POST data). + */ +void purple_http_request_set_contents(PurpleHttpRequest *request, + const gchar *contents, int length); + +/** + * purple_http_request_set_contents_reader: + * @request: The request. + * @reader: (scope call): The reader callback. + * @contents_length: The size of all contents. + * @user_data: The user data to pass to the callback function. + * + * Sets contents reader for HTTP request, used mainly for possible large + * uploads. + */ +void purple_http_request_set_contents_reader(PurpleHttpRequest *request, + PurpleHttpContentReader reader, int contents_length, gpointer user_data); + +/** + * purple_http_request_set_response_writer: + * @request: The request. + * @writer: (scope call): The writer callback, or %NULL to remove existing. + * @user_data: The user data to pass to the callback function. + * + * Set contents writer for HTTP response. + */ +void purple_http_request_set_response_writer(PurpleHttpRequest *request, + PurpleHttpContentWriter writer, gpointer user_data); + +/** + * purple_http_request_set_timeout: + * @request: The request. + * @timeout: Time (in seconds) after that timeout will be cancelled, + * -1 for infinite time. + * + * Set maximum amount of time, that request is allowed to run. + */ +void purple_http_request_set_timeout(PurpleHttpRequest *request, int timeout); + +/** + * purple_http_request_get_timeout: + * @request: The request. + * + * Get maximum amount of time, that request is allowed to run. + * + * Returns: Timeout currently set (-1 for infinite). + */ +int purple_http_request_get_timeout(PurpleHttpRequest *request); + +/** + * purple_http_request_set_max_redirects: + * @request: The request. + * @max_redirects: Maximum amount of redirects, or -1 for unlimited. + * + * Sets maximum amount of redirects. + */ +void purple_http_request_set_max_redirects(PurpleHttpRequest *request, + int max_redirects); + +/** + * purple_http_request_get_max_redirects: + * @request: The request. + * + * Gets maximum amount of redirects. + * + * Returns: Current maximum amount of redirects (-1 for unlimited). + */ +int purple_http_request_get_max_redirects(PurpleHttpRequest *request); + +/** + * purple_http_request_set_cookie_jar: + * @request: The request. + * @cookie_jar: The cookie jar. + * + * Sets cookie jar used for the request. + */ +void purple_http_request_set_cookie_jar(PurpleHttpRequest *request, + PurpleHttpCookieJar *cookie_jar); + +/** + * purple_http_request_get_cookie_jar: + * @request: The request. + * + * Gets cookie jar used for the request. + * + * Returns: The cookie jar. + */ +PurpleHttpCookieJar * purple_http_request_get_cookie_jar( + PurpleHttpRequest *request); + +/** + * purple_http_request_set_http11: + * @request: The request. + * @http11: TRUE for HTTP/1.1, FALSE for HTTP/1.0. + * + * Sets HTTP version to use. + */ +void purple_http_request_set_http11(PurpleHttpRequest *request, + gboolean http11); + +/** + * purple_http_request_is_http11: + * @request: The request. + * + * Gets used HTTP version. + * + * Returns: TRUE, if we use HTTP/1.1, FALSE for HTTP/1.0. + */ +gboolean purple_http_request_is_http11(PurpleHttpRequest *request); + +/** + * purple_http_request_set_max_len: + * @request: The request. + * @max_len: Maximum length of response to read (-1 for the maximum + * supported amount). + * + * Sets maximum length of response content to read. + * + * Headers length doesn't count here. + * + */ +void purple_http_request_set_max_len(PurpleHttpRequest *request, int max_len); + +/** + * purple_http_request_get_max_len: + * @request: The request. + * + * Gets maximum length of response content to read. + * + * Returns: Maximum length of response to read, or -1 if unlimited. + */ +int purple_http_request_get_max_len(PurpleHttpRequest *request); + +/** + * purple_http_request_header_set: + * @request: The request. + * @key: A header to be set. + * @value: A value to set, or NULL to remove specified header. + * + * Sets (replaces, if exists) specified HTTP request header with provided value. + * + * See purple_http_request_header_add(). + */ +void purple_http_request_header_set(PurpleHttpRequest *request, + const gchar *key, const gchar *value); + +/** + * purple_http_request_header_set_printf: + * @request: The request. + * @key: A header to be set. + * @format: The format string. + * + * Constructs and sets (replaces, if exists) specified HTTP request header. + */ +void purple_http_request_header_set_printf(PurpleHttpRequest *request, + const gchar *key, const gchar *format, ...) G_GNUC_PRINTF(3, 4); + +/** + * purple_http_request_header_add: + * @request: The request. + * @key: A header to be set. + * @value: A value to set. + * + * Adds (without replacing, if exists) an HTTP request header. + * + * See purple_http_request_header_set(). + */ +void purple_http_request_header_add(PurpleHttpRequest *request, + const gchar *key, const gchar *value); + + +/**************************************************************************/ +/* HTTP Keep-Alive pool API */ +/**************************************************************************/ + +/** + * purple_http_keepalive_pool_new: + * + * Creates a new HTTP Keep-Alive pool. + */ +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_new(void); + +/** + * purple_http_keepalive_pool_ref: + * @pool: The HTTP Keep-Alive pool. + * + * Increment the reference count. + */ +void +purple_http_keepalive_pool_ref(PurpleHttpKeepalivePool *pool); + +/** + * purple_http_keepalive_pool_unref: + * @pool: The HTTP Keep-Alive pool. + * + * Decrement the reference count. + * + * If the reference count reaches zero, the pool will be freed and all + * connections will be closed. + * + * Returns: @pool or %NULL if the reference count reached zero. + */ +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_unref(PurpleHttpKeepalivePool *pool); + +/** + * purple_http_keepalive_pool_set_limit_per_host: + * @pool: The HTTP Keep-Alive pool. + * @limit: The new limit, 0 for unlimited. + * + * Sets maximum allowed number of connections to specific host-triple (is_ssl + + * hostname + port). + */ +void +purple_http_keepalive_pool_set_limit_per_host(PurpleHttpKeepalivePool *pool, + guint limit); + +/** + * purple_http_keepalive_pool_get_limit_per_host: + * @pool: The HTTP Keep-Alive pool. + * + * Gets maximum allowed number of connections to specific host-triple (is_ssl + + * hostname + port). + * + * Returns: The limit. + */ +guint +purple_http_keepalive_pool_get_limit_per_host(PurpleHttpKeepalivePool *pool); + + +/**************************************************************************/ +/* HTTP connection set API */ +/**************************************************************************/ + +PurpleHttpConnectionSet * +purple_http_connection_set_new(void); + +void +purple_http_connection_set_destroy(PurpleHttpConnectionSet *set); + +void +purple_http_connection_set_add(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn); + + +/**************************************************************************/ +/* HTTP response API */ +/**************************************************************************/ + +/** + * purple_http_response_is_successful: + * @response: The response. + * + * Checks, if HTTP request was performed successfully. + * + * Returns: TRUE, if request was performed successfully. + */ +gboolean purple_http_response_is_successful(PurpleHttpResponse *response); + +/** + * purple_http_response_get_code: + * @response: The response. + * + * Gets HTTP response code. + * + * Returns: HTTP response code. + */ +int purple_http_response_get_code(PurpleHttpResponse *response); + +/** + * purple_http_response_get_error: + * @response: The response. + * + * Gets error description. + * + * Returns: Localized error description or NULL, if there was no error. + */ +const gchar * purple_http_response_get_error(PurpleHttpResponse *response); + +/** + * purple_http_response_get_data_len: + * @response: The response. + * + * Gets HTTP response data length. + * + * Returns: Data length; + */ +gsize purple_http_response_get_data_len(PurpleHttpResponse *response); + +/** + * purple_http_response_get_data: + * @response: The response. + * @len: Return address for the size of the data. Can be NULL. + * + * Gets HTTP response data. + * + * Response data is not written, if writer callback was set for request. + * + * Returns: The data. + */ +const gchar * purple_http_response_get_data(PurpleHttpResponse *response, size_t *len); + +/** + * purple_http_response_get_all_headers: + * @response: The response. + * + * Gets all headers got with response. + * + * Returns: GList of PurpleKeyValuePair, which keys are header field + * names (gchar*) and values are its contents (gchar*). + */ +const GList * purple_http_response_get_all_headers(PurpleHttpResponse *response); + +/** + * purple_http_response_get_headers_by_name: + * @response: The response. + * @name: The name of header field. + * + * Gets all headers with specified name got with response. + * + * Returns: GList of header field records contents (gchar*). + */ +const GList * purple_http_response_get_headers_by_name( + PurpleHttpResponse *response, const gchar *name); + +/** + * purple_http_response_get_header: + * @response: The response. + * @name: The name of header field. + * + * Gets one header contents with specified name got with response. + * + * To get all headers with the same name, use + * purple_http_response_get_headers_by_name instead. + * + * Returns: Header field contents or NULL, if there is no such one. + */ +const gchar * purple_http_response_get_header(PurpleHttpResponse *response, + const gchar *name); + + +/**************************************************************************/ +/* HTTP Subsystem */ +/**************************************************************************/ + +/** + * purple_http_init: + * + * Initializes the http subsystem. + */ +void purple_http_init(void); + +/** + * purple_http_uninit: + * + * Uninitializes the http subsystem. + */ +void purple_http_uninit(void); + +G_END_DECLS + +#endif /* _PURPLE_HTTP_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/Makefile.am b/pidgin/libpurple/protocols/facebook/Makefile.am new file mode 100644 index 0000000..268e8ef --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/Makefile.am @@ -0,0 +1,52 @@ +EXTRA_DIST = \ + Makefile.mingw + +pkgdir = @PURPLE_PLUGINDIR@ + +FACEBOOKSOURCES = \ + api.c \ + api.h \ + data.c \ + data.h \ + facebook.h \ + facebook.c \ + http.c \ + http.h \ + id.h \ + json.c \ + json.h \ + mqtt.c \ + mqtt.h \ + thrift.c \ + thrift.h \ + util.c \ + util.h + +AM_CFLAGS = $(st) + +libfacebook_la_LDFLAGS = -module @PLUGIN_LDFLAGS@ + +if STATIC_FACEBOOK + +st = -DPURPLE_STATIC_PRPL +noinst_LTLIBRARIES = libfacebook.la +libfacebook_la_SOURCES = $(FACEBOOKSOURCES) +libfacebook_la_CFLAGS = $(AM_CFLAGS) + +else + +st = +pkg_LTLIBRARIES = libfacebook.la +libfacebook_la_SOURCES = $(FACEBOOKSOURCES) +libfacebook_la_LIBADD = @PURPLE_LIBS@ $(JSON_LIBS) + +endif + +AM_CPPFLAGS = \ + -I$(top_srcdir)/libpurple \ + -I$(top_builddir)/libpurple \ + -I$(top_srcdir) \ + $(GLIB_CFLAGS) \ + $(JSON_CFLAGS) \ + $(GPLUGIN_CFLAGS) \ + $(DEBUG_CFLAGS) diff --git a/pidgin/libpurple/protocols/facebook/Makefile.mingw b/pidgin/libpurple/protocols/facebook/Makefile.mingw new file mode 100644 index 0000000..c2aaad8 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/Makefile.mingw @@ -0,0 +1,94 @@ +# +# Makefile.mingw +# +# Description: Makefile for win32 (mingw) version of libfacebook +# + +PIDGIN_TREE_TOP := ../../.. +include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak + +TARGET = libfacebook +TYPE = PLUGIN + +# Static or Plugin... +ifeq ($(TYPE),STATIC) + DEFINES += -DSTATIC + DLL_INSTALL_DIR = $(PURPLE_INSTALL_DIR) +else +ifeq ($(TYPE),PLUGIN) + DLL_INSTALL_DIR = $(PURPLE_INSTALL_PLUGINS_DIR) +endif +endif + +## +## INCLUDE PATHS +## +INCLUDE_PATHS += -I. \ + -I$(GTK_TOP)/include \ + -I$(GTK_TOP)/include/glib-2.0 \ + -I$(GTK_TOP)/lib/glib-2.0/include \ + -I$(JSON_GLIB_TOP)/include/json-glib-1.0 \ + -I$(PURPLE_TOP) \ + -I$(PURPLE_TOP)/win32 \ + -I$(GPLUGIN_TOP) \ + -I$(PIDGIN_TREE_TOP) + +LIB_PATHS += -L$(GTK_TOP)/lib \ + -L$(JSON_GLIB_TOP)/lib \ + -L$(GPLUGIN_TOP) \ + -L$(PURPLE_TOP) + +## +## SOURCES, OBJECTS +## +C_SRC = \ + api.c \ + data.c \ + facebook.c \ + http.c \ + json.c \ + mqtt.c \ + thrift.c \ + util.c + +OBJECTS = $(C_SRC:%.c=%.o) + +## +## LIBRARIES +## +LIBS = \ + -lglib-2.0 \ + -lgio-2.0 \ + -lgobject-2.0 \ + -lws2_32 \ + -lintl \ + -lgplugin \ + -ljson-glib-1.0 \ + -lz \ + -lpurple + +include $(PIDGIN_COMMON_RULES) + +## +## TARGET DEFINITIONS +## +.PHONY: all install clean + +all: $(TARGET).dll + +install: all $(DLL_INSTALL_DIR) + cp $(TARGET).dll $(DLL_INSTALL_DIR) + +$(OBJECTS): $(PURPLE_CONFIG_H) + +$(TARGET).dll: $(PURPLE_DLL).a $(OBJECTS) + $(CC) -shared $(OBJECTS) $(LIB_PATHS) $(LIBS) $(DLL_LD_FLAGS) -o $(TARGET).dll + +## +## CLEAN RULES +## +clean: + rm -f $(OBJECTS) + rm -f $(TARGET).dll + +include $(PIDGIN_COMMON_TARGETS) diff --git a/pidgin/libpurple/protocols/facebook/api.c b/pidgin/libpurple/protocols/facebook/api.c new file mode 100644 index 0000000..163a488 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/api.c @@ -0,0 +1,3501 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include <json-glib/json-glib.h> +#include <stdarg.h> +#include <string.h> + +#include "glibcompat.h" + +#include "api.h" +#include "http.h" +#include "json.h" +#include "thrift.h" +#include "util.h" + +typedef struct _FbApiData FbApiData; + +enum +{ + PROP_0, + + PROP_CID, + PROP_DID, + PROP_MID, + PROP_STOKEN, + PROP_TOKEN, + PROP_UID, + + PROP_N +}; + +struct _FbApiPrivate +{ + FbMqtt *mqtt; + FbHttpConns *cons; + PurpleConnection *gc; + GHashTable *data; + gboolean retrying; + + FbId uid; + gint64 sid; + guint64 mid; + gchar *cid; + gchar *did; + gchar *stoken; + gchar *token; + + GQueue *msgs; + gboolean invisible; + guint unread; + FbId lastmid; + gchar *contacts_delta; +}; + +struct _FbApiData +{ + gpointer data; + GDestroyNotify func; +}; + +static void +fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg); + +static void +fb_api_contacts_after(FbApi *api, const gchar *cursor); + +static void +fb_api_message_send(FbApi *api, FbApiMessage *msg); + +static void +fb_api_sticker(FbApi *api, FbId sid, FbApiMessage *msg); + +void +fb_api_contacts_delta(FbApi *api, const gchar *delta_cursor); + +G_DEFINE_TYPE(FbApi, fb_api, G_TYPE_OBJECT); + +static void +fb_api_set_property(GObject *obj, guint prop, const GValue *val, + GParamSpec *pspec) +{ + FbApiPrivate *priv = FB_API(obj)->priv; + + switch (prop) { + case PROP_CID: + g_free(priv->cid); + priv->cid = g_value_dup_string(val); + break; + case PROP_DID: + g_free(priv->did); + priv->did = g_value_dup_string(val); + break; + case PROP_MID: + priv->mid = g_value_get_uint64(val); + break; + case PROP_STOKEN: + g_free(priv->stoken); + priv->stoken = g_value_dup_string(val); + break; + case PROP_TOKEN: + g_free(priv->token); + priv->token = g_value_dup_string(val); + break; + case PROP_UID: + priv->uid = g_value_get_int64(val); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec); + break; + } +} + +static void +fb_api_get_property(GObject *obj, guint prop, GValue *val, GParamSpec *pspec) +{ + FbApiPrivate *priv = FB_API(obj)->priv; + + switch (prop) { + case PROP_CID: + g_value_set_string(val, priv->cid); + break; + case PROP_DID: + g_value_set_string(val, priv->did); + break; + case PROP_MID: + g_value_set_uint64(val, priv->mid); + break; + case PROP_STOKEN: + g_value_set_string(val, priv->stoken); + break; + case PROP_TOKEN: + g_value_set_string(val, priv->token); + break; + case PROP_UID: + g_value_set_int64(val, priv->uid); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec); + break; + } +} + + +static void +fb_api_dispose(GObject *obj) +{ + FbApiData *fata; + FbApiPrivate *priv = FB_API(obj)->priv; + GHashTableIter iter; + + fb_http_conns_cancel_all(priv->cons); + g_hash_table_iter_init(&iter, priv->data); + + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &fata)) { + fata->func(fata->data); + g_free(fata); + } + + if (G_UNLIKELY(priv->mqtt != NULL)) { + g_object_unref(priv->mqtt); + } + + fb_http_conns_free(priv->cons); + g_hash_table_destroy(priv->data); + g_queue_free_full(priv->msgs, (GDestroyNotify) fb_api_message_free); + + g_free(priv->cid); + g_free(priv->did); + g_free(priv->stoken); + g_free(priv->token); + g_free(priv->contacts_delta); +} + +static void +fb_api_class_init(FbApiClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + GParamSpec *props[PROP_N] = {NULL}; + + gklass->set_property = fb_api_set_property; + gklass->get_property = fb_api_get_property; + gklass->dispose = fb_api_dispose; + g_type_class_add_private(klass, sizeof (FbApiPrivate)); + + /** + * FbApi:cid: + * + * The client identifier for MQTT. This value should be saved + * and loaded for persistence. + */ + props[PROP_CID] = g_param_spec_string( + "cid", + "Client ID", + "Client identifier for MQTT", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:did: + * + * The device identifier for the MQTT message queue. This value + * should be saved and loaded for persistence. + */ + props[PROP_DID] = g_param_spec_string( + "did", + "Device ID", + "Device identifier for the MQTT message queue", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:mid: + * + * The MQTT identifier. This value should be saved and loaded + * for persistence. + */ + props[PROP_MID] = g_param_spec_uint64( + "mid", + "MQTT ID", + "MQTT identifier", + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE); + + /** + * FbApi:stoken: + * + * The synchronization token for the MQTT message queue. This + * value should be saved and loaded for persistence. + */ + props[PROP_STOKEN] = g_param_spec_string( + "stoken", + "Sync Token", + "Synchronization token for the MQTT message queue", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:token: + * + * The access token for authentication. This value should be + * saved and loaded for persistence. + */ + props[PROP_TOKEN] = g_param_spec_string( + "token", + "Access Token", + "Access token for authentication", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:uid: + * + * The #FbId of the user of the #FbApi. + */ + props[PROP_UID] = g_param_spec_int64( + "uid", + "User ID", + "User identifier", + 0, G_MAXINT64, 0, + G_PARAM_READWRITE); + g_object_class_install_properties(gklass, PROP_N, props); + + /** + * FbApi::auth: + * @api: The #FbApi. + * + * Emitted upon the successful completion of the authentication + * process. This is emitted as a result of #fb_api_auth(). + */ + g_signal_new("auth", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * FbApi::connect: + * @api: The #FbApi. + * + * Emitted upon the successful completion of the connection + * process. This is emitted as a result of #fb_api_connect(). + */ + g_signal_new("connect", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * FbApi::contact: + * @api: The #FbApi. + * @user: The #FbApiUser. + * + * Emitted upon the successful reply of a contact request. This + * is emitted as a result of #fb_api_contact(). + */ + g_signal_new("contact", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::contacts: + * @api: The #FbApi. + * @users: The #GSList of #FbApiUser's. + * @complete: #TRUE if the list is fetched, otherwise #FALSE. + * + * Emitted upon the successful reply of a contacts request. + * This is emitted as a result of #fb_api_contacts(). This can + * be emitted multiple times before the entire contacts list + * has been fetched. Use @complete for detecting the completion + * status of the list fetch. + */ + g_signal_new("contacts", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, G_TYPE_POINTER, G_TYPE_BOOLEAN); + + /** + * FbApi::contacts-delta: + * @api: The #FbApi. + * @added: The #GSList of added #FbApiUser's. + * @removed: The #GSList of strings with removed user ids. + * + * Like 'contacts', but only the deltas. + */ + g_signal_new("contacts-delta", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, G_TYPE_POINTER, G_TYPE_POINTER); + + /** + * FbApi::error: + * @api: The #FbApi. + * @error: The #GError. + * + * Emitted whenever an error is hit within the #FbApi. This + * should disconnect the #FbApi with #fb_api_disconnect(). + */ + g_signal_new("error", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::events: + * @api: The #FbApi. + * @events: The #GSList of #FbApiEvent's. + * + * Emitted upon incoming events from the stream. + */ + g_signal_new("events", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::messages: + * @api: The #FbApi. + * @msgs: The #GSList of #FbApiMessage's. + * + * Emitted upon incoming messages from the stream. + */ + g_signal_new("messages", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::presences: + * @api: The #FbApi. + * @press: The #GSList of #FbApiPresence's. + * + * Emitted upon incoming presences from the stream. + */ + g_signal_new("presences", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::thread: + * @api: The #FbApi. + * @thrd: The #FbApiThread. + * + * Emitted upon the successful reply of a thread request. This + * is emitted as a result of #fb_api_thread(). + */ + g_signal_new("thread", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::thread-create: + * @api: The #FbApi. + * @tid: The thread #FbId. + * + * Emitted upon the successful reply of a thread creation + * request. This is emitted as a result of + * #fb_api_thread_create(). + */ + g_signal_new("thread-create", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, FB_TYPE_ID); + + /** + * FbApi::thread-kicked: + * @api: The #FbApi. + * @thrd: The #FbApiThread. + * + * Emitted upon the reply of a thread request when the user is no longer + * part of that thread. This is emitted as a result of #fb_api_thread(). + */ + g_signal_new("thread-kicked", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::threads: + * @api: The #FbApi. + * @thrds: The #GSList of #FbApiThread's. + * + * Emitted upon the successful reply of a threads request. This + * is emitted as a result of #fb_api_threads(). + */ + g_signal_new("threads", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::typing: + * @api: The #FbApi. + * @typg: The #FbApiTyping. + * + * Emitted upon an incoming typing state from the stream. + */ + g_signal_new("typing", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_POINTER); +} + +static void +fb_api_init(FbApi *api) +{ + FbApiPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(api, FB_TYPE_API, FbApiPrivate); + api->priv = priv; + + priv->cons = fb_http_conns_new(); + priv->msgs = g_queue_new(); + priv->data = g_hash_table_new_full(g_direct_hash, g_direct_equal, + NULL, NULL); +} + +GQuark +fb_api_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-api-error-quark"); + } + + return q; +} + +static void +fb_api_data_set(FbApi *api, gpointer handle, gpointer data, + GDestroyNotify func) +{ + FbApiData *fata; + FbApiPrivate *priv = api->priv; + + fata = g_new0(FbApiData, 1); + fata->data = data; + fata->func = func; + g_hash_table_replace(priv->data, handle, fata); +} + +static gpointer +fb_api_data_take(FbApi *api, gconstpointer handle) +{ + FbApiData *fata; + FbApiPrivate *priv = api->priv; + gpointer data; + + fata = g_hash_table_lookup(priv->data, handle); + + if (fata == NULL) { + return NULL; + } + + data = fata->data; + g_hash_table_remove(priv->data, handle); + g_free(fata); + return data; +} + +static gboolean +fb_api_json_chk(FbApi *api, gconstpointer data, gssize size, JsonNode **node) +{ + const gchar *str; + FbApiError errc = FB_API_ERROR_GENERAL; + FbApiPrivate *priv; + FbJsonValues *values; + gboolean success = TRUE; + gchar *msg; + GError *err = NULL; + gint64 code; + guint i; + JsonNode *root; + + static const gchar *exprs[] = { + "$.error.message", + "$.error.summary", + "$.error_msg", + "$.errorCode", + "$.failedSend.errorMessage", + }; + + g_return_val_if_fail(FB_IS_API(api), FALSE); + priv = api->priv; + + if (G_UNLIKELY(size == 0)) { + fb_api_error(api, FB_API_ERROR_GENERAL, _("Empty JSON data")); + return FALSE; + } + + fb_util_debug(FB_UTIL_DEBUG_INFO, "Parsing JSON: %.*s\n", + (gint) size, (const gchar *) data); + + root = fb_json_node_new(data, size, &err); + FB_API_ERROR_EMIT(api, err, return FALSE); + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, "$.error_code"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.error.type"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.errorCode"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return FALSE + ); + + code = fb_json_values_next_int(values, 0); + str = fb_json_values_next_str(values, NULL); + + if (purple_strequal(str, "OAuthException") || (code == 401)) { + errc = FB_API_ERROR_AUTH; + success = FALSE; + + g_free(priv->stoken); + priv->stoken = NULL; + + g_free(priv->token); + priv->token = NULL; + } + + /* 509 is used for "invalid attachment id" */ + if (code == 509) { + errc = FB_API_ERROR_NONFATAL; + success = FALSE; + } + + str = fb_json_values_next_str(values, NULL); + + if (purple_strequal(str, "ERROR_QUEUE_NOT_FOUND") || + purple_strequal(str, "ERROR_QUEUE_LOST")) + { + errc = FB_API_ERROR_QUEUE; + success = FALSE; + + g_free(priv->stoken); + priv->stoken = NULL; + } + + g_object_unref(values); + + for (msg = NULL, i = 0; i < G_N_ELEMENTS(exprs); i++) { + msg = fb_json_node_get_str(root, exprs[i], NULL); + + if (msg != NULL) { + success = FALSE; + break; + } + } + + if (!success && (msg == NULL)) { + msg = g_strdup(_("Unknown error")); + } + + if (msg != NULL) { + fb_api_error(api, errc, "%s", msg); + json_node_free(root); + g_free(msg); + return FALSE; + } + + if (node != NULL) { + *node = root; + } else { + json_node_free(root); + } + + return TRUE; +} + +static gboolean +fb_api_http_chk(FbApi *api, PurpleHttpConnection *con, PurpleHttpResponse *res, + JsonNode **root) +{ + const gchar *data; + const gchar *msg; + FbApiPrivate *priv = api->priv; + gchar *emsg; + GError *err = NULL; + gint code; + gsize size; + + if (fb_http_conns_is_canceled(priv->cons)) { + return FALSE; + } + + msg = purple_http_response_get_error(res); + code = purple_http_response_get_code(res); + data = purple_http_response_get_data(res, &size); + fb_http_conns_remove(priv->cons, con); + + if (msg != NULL) { + emsg = g_strdup_printf("%s (%d)", msg, code); + } else { + emsg = g_strdup_printf("%d", code); + } + + fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Response (%p):", con); + fb_util_debug(FB_UTIL_DEBUG_INFO, " Response Error: %s", emsg); + g_free(emsg); + + if (G_LIKELY(size > 0)) { + fb_util_debug(FB_UTIL_DEBUG_INFO, " Response Data: %.*s", + (gint) size, data); + } + + if (fb_http_error_chk(res, &err) && (root == NULL)) { + return TRUE; + } + + /* Rudimentary check to prevent wrongful error parsing */ + if ((size < 2) || (data[0] != '{') || (data[size - 1] != '}')) { + FB_API_ERROR_EMIT(api, err, return FALSE); + } + + if (!fb_api_json_chk(api, data, size, root)) { + if (G_UNLIKELY(err != NULL)) { + g_error_free(err); + } + + return FALSE; + } + + FB_API_ERROR_EMIT(api, err, return FALSE); + return TRUE; +} + +static PurpleHttpConnection * +fb_api_http_req(FbApi *api, const gchar *url, const gchar *name, + const gchar *method, FbHttpParams *params, + PurpleHttpCallback callback) +{ + FbApiPrivate *priv = api->priv; + gchar *data; + gchar *key; + gchar *val; + GList *keys; + GList *l; + GString *gstr; + PurpleHttpConnection *ret; + PurpleHttpRequest *req; + + fb_http_params_set_str(params, "api_key", FB_API_KEY); + fb_http_params_set_str(params, "device_id", priv->did); + fb_http_params_set_str(params, "fb_api_req_friendly_name", name); + fb_http_params_set_str(params, "format", "json"); + fb_http_params_set_str(params, "method", method); + + val = fb_util_get_locale(); + fb_http_params_set_str(params, "locale", val); + g_free(val); + + req = purple_http_request_new(url); + purple_http_request_set_max_len(req, -1); + purple_http_request_set_method(req, "POST"); + + /* Ensure an old signature is not computed */ + g_hash_table_remove(params, "sig"); + + gstr = g_string_new(NULL); + keys = g_hash_table_get_keys(params); + keys = g_list_sort(keys, (GCompareFunc) g_ascii_strcasecmp); + + for (l = keys; l != NULL; l = l->next) { + key = l->data; + val = g_hash_table_lookup(params, key); + g_string_append_printf(gstr, "%s=%s", key, val); + } + + g_string_append(gstr, FB_API_SECRET); + data = g_compute_checksum_for_string(G_CHECKSUM_MD5, gstr->str, + gstr->len); + fb_http_params_set_str(params, "sig", data); + g_string_free(gstr, TRUE); + g_list_free(keys); + g_free(data); + + if (priv->token != NULL) { + data = g_strdup_printf("OAuth %s", priv->token); + purple_http_request_header_set(req, "Authorization", data); + g_free(data); + } + + purple_http_request_header_set(req, "User-Agent", FB_API_AGENT); + purple_http_request_header_set(req, "Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + + data = fb_http_params_close(params, NULL); + purple_http_request_set_contents(req, data, -1); + ret = purple_http_request(priv->gc, req, callback, api); + fb_http_conns_add(priv->cons, ret); + purple_http_request_unref(req); + + fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Request (%p):", ret); + fb_util_debug(FB_UTIL_DEBUG_INFO, " Request URL: %s", url); + fb_util_debug(FB_UTIL_DEBUG_INFO, " Request Data: %s", data); + + g_free(data); + return ret; +} + +static PurpleHttpConnection * +fb_api_http_query(FbApi *api, gint64 query, JsonBuilder *builder, + PurpleHttpCallback hcb) +{ + const gchar *name; + FbHttpParams *prms; + gchar *json; + + switch (query) { + case FB_API_QUERY_CONTACT: + name = "UsersQuery"; + break; + case FB_API_QUERY_CONTACTS: + name = "FetchContactsFullQuery"; + break; + case FB_API_QUERY_CONTACTS_AFTER: + name = "FetchContactsFullWithAfterQuery"; + break; + case FB_API_QUERY_CONTACTS_DELTA: + name = "FetchContactsDeltaQuery"; + break; + case FB_API_QUERY_STICKER: + name = "FetchStickersWithPreviewsQuery"; + break; + case FB_API_QUERY_THREAD: + name = "ThreadQuery"; + break; + case FB_API_QUERY_SEQ_ID: + case FB_API_QUERY_THREADS: + name = "ThreadListQuery"; + break; + case FB_API_QUERY_XMA: + name = "XMAQuery"; + break; + default: + g_return_val_if_reached(NULL); + return NULL; + } + + prms = fb_http_params_new(); + json = fb_json_bldr_close(builder, JSON_NODE_OBJECT, NULL); + + fb_http_params_set_strf(prms, "query_id", "%" G_GINT64_FORMAT, query); + fb_http_params_set_str(prms, "query_params", json); + g_free(json); + + return fb_api_http_req(api, FB_API_URL_GQL, name, "get", prms, hcb); +} + +static void +fb_api_cb_http_bool(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *hata; + FbApi *api = data; + + if (!fb_api_http_chk(api, con, res, NULL)) { + return; + } + + hata = purple_http_response_get_data(res, NULL); + + if (!purple_strequal(hata, "true")) { + fb_api_error(api, FB_API_ERROR, + _("Failed generic API operation")); + } +} + +static void +fb_api_cb_mqtt_error(FbMqtt *mqtt, GError *error, gpointer data) +{ + FbApi *api = data; + FbApiPrivate *priv = api->priv; + + if (!priv->retrying) { + priv->retrying = TRUE; + fb_util_debug_info("Attempting to reconnect the MQTT stream..."); + fb_api_connect(api, priv->invisible); + } else { + g_signal_emit_by_name(api, "error", error); + } +} + +static void +fb_api_cb_mqtt_open(FbMqtt *mqtt, gpointer data) +{ + const GByteArray *bytes; + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbThrift *thft; + GByteArray *cytes; + GError *err = NULL; + + static guint8 flags = FB_MQTT_CONNECT_FLAG_USER | + FB_MQTT_CONNECT_FLAG_PASS | + FB_MQTT_CONNECT_FLAG_CLR; + + thft = fb_thrift_new(NULL, 0); + + /* Write the client identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 1, 0); + fb_thrift_write_str(thft, priv->cid); + + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRUCT, 4, 1); + + /* Write the user identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 1, 0); + fb_thrift_write_i64(thft, priv->uid); + + /* Write the information string */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 2, 1); + fb_thrift_write_str(thft, FB_API_MQTT_AGENT); + + /* Write the UNKNOWN ("cp"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 3, 2); + fb_thrift_write_i64(thft, 23); + + /* Write the UNKNOWN ("ecp"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 4, 3); + fb_thrift_write_i64(thft, 26); + + /* Write the UNKNOWN */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 5, 4); + fb_thrift_write_i32(thft, 1); + + /* Write the UNKNOWN ("no_auto_fg"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 6, 5); + fb_thrift_write_bool(thft, TRUE); + + /* Write the visibility state */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 7, 6); + fb_thrift_write_bool(thft, !priv->invisible); + + /* Write the device identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 8, 7); + fb_thrift_write_str(thft, priv->did); + + /* Write the UNKNOWN ("fg"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 9, 8); + fb_thrift_write_bool(thft, TRUE); + + /* Write the UNKNOWN ("nwt"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 10, 9); + fb_thrift_write_i32(thft, 1); + + /* Write the UNKNOWN ("nwst"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 11, 10); + fb_thrift_write_i32(thft, 0); + + /* Write the MQTT identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 12, 11); + fb_thrift_write_i64(thft, priv->mid); + + /* Write the UNKNOWN */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_LIST, 14, 12); + fb_thrift_write_list(thft, FB_THRIFT_TYPE_I32, 0); + fb_thrift_write_stop(thft); + + /* Write the token */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 15, 14); + fb_thrift_write_str(thft, priv->token); + + /* Write the STOP for the struct */ + fb_thrift_write_stop(thft); + + bytes = fb_thrift_get_bytes(thft); + cytes = fb_util_zlib_deflate(bytes, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(thft); + return; + ); + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes, "Writing connect"); + fb_mqtt_connect(mqtt, flags, cytes); + + g_byte_array_free(cytes, TRUE); + g_object_unref(thft); +} + +static void +fb_api_connect_queue(FbApi *api) +{ + FbApiMessage *msg; + FbApiPrivate *priv = api->priv; + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_int(bldr, "delta_batch_size", 125); + fb_json_bldr_add_int(bldr, "max_deltas_able_to_process", 1250); + fb_json_bldr_add_int(bldr, "sync_api_version", 3); + fb_json_bldr_add_str(bldr, "encoding", "JSON"); + + if (priv->stoken == NULL) { + fb_json_bldr_add_int(bldr, "initial_titan_sequence_id", + priv->sid); + fb_json_bldr_add_str(bldr, "device_id", priv->did); + fb_json_bldr_add_int(bldr, "entity_fbid", priv->uid); + + fb_json_bldr_obj_begin(bldr, "queue_params"); + fb_json_bldr_add_str(bldr, "buzz_on_deltas_enabled", "false"); + + fb_json_bldr_obj_begin(bldr, "graphql_query_hashes"); + fb_json_bldr_add_str(bldr, "xma_query_id", + G_STRINGIFY(FB_API_QUERY_XMA)); + fb_json_bldr_obj_end(bldr); + + fb_json_bldr_obj_begin(bldr, "graphql_query_params"); + fb_json_bldr_obj_begin(bldr, G_STRINGIFY(FB_API_QUERY_XMA)); + fb_json_bldr_add_str(bldr, "xma_id", "<ID>"); + fb_json_bldr_obj_end(bldr); + fb_json_bldr_obj_end(bldr); + fb_json_bldr_obj_end(bldr); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/messenger_sync_create_queue", "%s", + json); + g_free(json); + return; + } + + fb_json_bldr_add_int(bldr, "last_seq_id", priv->sid); + fb_json_bldr_add_str(bldr, "sync_token", priv->stoken); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/messenger_sync_get_diffs", "%s", json); + g_signal_emit_by_name(api, "connect"); + g_free(json); + + if (!g_queue_is_empty(priv->msgs)) { + msg = g_queue_peek_head(priv->msgs); + fb_api_message_send(api, msg); + } + + if (priv->retrying) { + priv->retrying = FALSE; + fb_util_debug_info("Reconnected the MQTT stream"); + } +} + +static void +fb_api_cb_seqid(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.viewer.message_threads.sync_sequence_id"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, + "$.viewer.message_threads.unread_count"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + str = fb_json_values_next_str(values, "0"); + priv->sid = g_ascii_strtoll(str, NULL, 10); + priv->unread = fb_json_values_next_int(values, 0); + + if (priv->sid == 0) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to get sync_sequence_id")); + } else { + fb_api_connect_queue(api); + } + + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_cb_mqtt_connect(FbMqtt *mqtt, gpointer data) +{ + FbApi *api = data; + FbApiPrivate *priv = api->priv; + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_bool(bldr, "foreground", TRUE); + fb_json_bldr_add_int(bldr, "keepalive_timeout", FB_MQTT_KA); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/foreground_state", "%s", json); + g_free(json); + + fb_mqtt_subscribe(mqtt, + "/inbox", 0, + "/mercury", 0, + "/messaging_events", 0, + "/orca_presence", 0, + "/orca_typing_notifications", 0, + "/pp", 0, + "/t_ms", 0, + "/t_p", 0, + "/t_rtc", 0, + "/webrtc", 0, + "/webrtc_response", 0, + NULL + ); + + /* Notifications seem to lead to some sort of sending rate limit */ + fb_mqtt_unsubscribe(mqtt, "/orca_message_notifications", NULL); + + if (priv->sid == 0) { + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "1", "0"); + fb_api_http_query(api, FB_API_QUERY_SEQ_ID, bldr, + fb_api_cb_seqid); + } else { + fb_api_connect_queue(api); + } +} + +static void +fb_api_cb_publish_mark(FbApi *api, GByteArray *pload) +{ + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE, "$.succeeded"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + if (!fb_json_values_next_bool(values, TRUE)) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to mark thread as read")); + } + + g_object_unref(values); + json_node_free(root); +} + +static GSList * +fb_api_event_parse(FbApi *api, FbApiEvent *event, GSList *events, + JsonNode *root, GError **error) +{ + const gchar *str; + FbApiEvent *devent; + FbJsonValues *values; + GError *err = NULL; + guint i; + + static const struct { + FbApiEventType type; + const gchar *expr; + } evtypes[] = { + { + FB_API_EVENT_TYPE_THREAD_USER_ADDED, + "$.log_message_data.added_participants" + }, { + FB_API_EVENT_TYPE_THREAD_USER_REMOVED, + "$.log_message_data.removed_participants" + } + }; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.log_message_type"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.author"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.log_message_data.name"); + fb_json_values_update(values, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + g_object_unref(values); + return events; + } + + str = fb_json_values_next_str(values, NULL); + + if (g_strcmp0(str, "log:thread-name") == 0) { + str = fb_json_values_next_str(values, ""); + str = strrchr(str, ':'); + + if (str != NULL) { + devent = fb_api_event_dup(event, FALSE); + devent->type = FB_API_EVENT_TYPE_THREAD_TOPIC; + devent->uid = FB_ID_FROM_STR(str + 1); + devent->text = fb_json_values_next_str_dup(values, NULL); + events = g_slist_prepend(events, devent); + } + } + + g_object_unref(values); + + for (i = 0; i < G_N_ELEMENTS(evtypes); i++) { + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$"); + fb_json_values_set_array(values, FALSE, evtypes[i].expr); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, ""); + str = strrchr(str, ':'); + + if (str != NULL) { + devent = fb_api_event_dup(event, FALSE); + devent->type = evtypes[i].type; + devent->uid = FB_ID_FROM_STR(str + 1); + events = g_slist_prepend(events, devent); + } + } + + g_object_unref(values); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + break; + } + } + + return events; +} + +static void +fb_api_cb_publish_mercury(FbApi *api, GByteArray *pload) +{ + const gchar *str; + FbApiEvent event; + FbJsonValues *values; + GError *err = NULL; + GSList *events = NULL; + JsonNode *root; + JsonNode *node; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.thread_fbid"); + fb_json_values_set_array(values, FALSE, "$.actions"); + + while (fb_json_values_update(values, &err)) { + fb_api_event_reset(&event, FALSE); + str = fb_json_values_next_str(values, "0"); + event.tid = FB_ID_FROM_STR(str); + + node = fb_json_values_get_root(values); + events = fb_api_event_parse(api, &event, events, node, &err); + } + + if (G_LIKELY(err == NULL)) { + events = g_slist_reverse(events); + g_signal_emit_by_name(api, "events", events); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(events, (GDestroyNotify) fb_api_event_free); + g_object_unref(values); + json_node_free(root); + +} + +static void +fb_api_cb_publish_typing(FbApi *api, GByteArray *pload) +{ + const gchar *str; + FbApiPrivate *priv = api->priv; + FbApiTyping typg; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.type"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.sender_fbid"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.state"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + str = fb_json_values_next_str(values, NULL); + + if (g_ascii_strcasecmp(str, "typ") == 0) { + typg.uid = fb_json_values_next_int(values, 0); + + if (typg.uid != priv->uid) { + typg.state = fb_json_values_next_int(values, 0); + g_signal_emit_by_name(api, "typing", &typg); + } + } + + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_cb_publish_ms_r(FbApi *api, GByteArray *pload) +{ + FbApiMessage *msg; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, TRUE, "$.succeeded"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + if (fb_json_values_next_bool(values, TRUE)) { + /* Pop and free the successful message */ + msg = g_queue_pop_head(priv->msgs); + fb_api_message_free(msg); + + if (!g_queue_is_empty(priv->msgs)) { + msg = g_queue_peek_head(priv->msgs); + fb_api_message_send(api, msg); + } + } else { + fb_api_error(api, FB_API_ERROR_GENERAL, + "Failed to send message"); + } + + g_object_unref(values); + json_node_free(root); +} + +static gchar * +fb_api_xma_parse(FbApi *api, const gchar *body, JsonNode *root, GError **error) +{ + const gchar *str; + const gchar *url; + FbHttpParams *params; + FbJsonValues *values; + gchar *text; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.story_attachment.target.__type__.name"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.story_attachment.url"); + fb_json_values_update(values, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + g_object_unref(values); + return NULL; + } + + str = fb_json_values_next_str(values, NULL); + url = fb_json_values_next_str(values, NULL); + + if ((str == NULL) || (url == NULL)) { + text = g_strdup(_("<Unsupported Attachment>")); + g_object_unref(values); + return text; + } + + if (purple_strequal(str, "ExternalUrl")) { + params = fb_http_params_new_parse(url, TRUE); + if (g_str_has_prefix(url, FB_API_FBRPC_PREFIX)) { + text = fb_http_params_dup_str(params, "target_url", NULL); + } else { + text = fb_http_params_dup_str(params, "u", NULL); + } + fb_http_params_free(params); + } else { + text = g_strdup(url); + } + + if (fb_http_urlcmp(body, text, FALSE)) { + g_free(text); + g_object_unref(values); + return NULL; + } + + g_object_unref(values); + return text; +} + +static GSList * +fb_api_message_parse_attach(FbApi *api, const gchar *mid, FbApiMessage *msg, + GSList *msgs, const gchar *body, JsonNode *root, + GError **error) +{ + const gchar *str; + FbApiMessage *dmsg; + FbId id; + FbJsonValues *values; + gchar *xma; + GError *err = NULL; + JsonNode *node; + JsonNode *xode; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.xmaGraphQL"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, "$.fbid"); + fb_json_values_set_array(values, FALSE, "$.attachments"); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + id = fb_json_values_next_int(values, 0); + dmsg = fb_api_message_dup(msg, FALSE); + fb_api_attach(api, id, mid, dmsg); + continue; + } + + node = fb_json_node_new(str, -1, &err); + + if (G_UNLIKELY(err != NULL)) { + break; + } + + xode = fb_json_node_get_nth(node, 0); + xma = fb_api_xma_parse(api, body, xode, &err); + + if (xma != NULL) { + dmsg = fb_api_message_dup(msg, FALSE); + dmsg->text = xma; + msgs = g_slist_prepend(msgs, dmsg); + } + + json_node_free(node); + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + } + + g_object_unref(values); + return msgs; +} + + +static GSList * +fb_api_cb_publish_ms_new_message(FbApi *api, JsonNode *root, GSList *msgs, GError **error); + +static GSList * +fb_api_cb_publish_ms_event(FbApi *api, JsonNode *root, GSList *events, FbApiEventType type, GError **error); + +static void +fb_api_cb_publish_ms(FbApi *api, GByteArray *pload) +{ + const gchar *data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + FbThrift *thft; + gchar *stoken; + GError *err = NULL; + GList *elms, *l; + GSList *msgs = NULL; + GSList *events = NULL; + guint size; + JsonNode *root; + JsonNode *node; + JsonArray *arr; + + static const struct { + const gchar *member; + FbApiEventType type; + gboolean is_message; + } event_types[] = { + {"deltaNewMessage", 0, 1}, + {"deltaThreadName", FB_API_EVENT_TYPE_THREAD_TOPIC, 0}, + {"deltaParticipantsAddedToGroupThread", FB_API_EVENT_TYPE_THREAD_USER_ADDED, 0}, + {"deltaParticipantLeftGroupThread", FB_API_EVENT_TYPE_THREAD_USER_REMOVED, 0}, + }; + + /* Read identifier string (for Facebook employees) */ + thft = fb_thrift_new(pload, 0); + fb_thrift_read_str(thft, NULL); + size = fb_thrift_get_pos(thft); + g_object_unref(thft); + + g_return_if_fail(size < pload->len); + data = (gchar *) pload->data + size; + size = pload->len - size; + + if (!fb_api_json_chk(api, data, size, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.lastIssuedSeqId"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.syncToken"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + priv->sid = fb_json_values_next_int(values, 0); + stoken = fb_json_values_next_str_dup(values, NULL); + g_object_unref(values); + + if (G_UNLIKELY(stoken != NULL)) { + g_free(priv->stoken); + priv->stoken = stoken; + g_signal_emit_by_name(api, "connect"); + json_node_free(root); + return; + } + + arr = fb_json_node_get_arr(root, "$.deltas", NULL); + elms = json_array_get_elements(arr); + + for (l = elms; l != NULL; l = l->next) { + guint i = 0; + JsonObject *o = json_node_get_object(l->data); + + for (i = 0; i < G_N_ELEMENTS(event_types); i++) { + if ((node = json_object_get_member(o, event_types[i].member))) { + if (event_types[i].is_message) { + msgs = fb_api_cb_publish_ms_new_message( + api, node, msgs, &err + ); + } else { + events = fb_api_cb_publish_ms_event( + api, node, events, event_types[i].type, &err + ); + } + } + } + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + g_list_free(elms); + json_array_unref(arr); + + if (G_LIKELY(err == NULL)) { + if (msgs) { + msgs = g_slist_reverse(msgs); + g_signal_emit_by_name(api, "messages", msgs); + } + + if (events) { + events = g_slist_reverse(events); + g_signal_emit_by_name(api, "events", events); + } + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_slist_free_full(events, (GDestroyNotify) fb_api_event_free); + json_node_free(root); +} + +static GSList * +fb_api_cb_publish_ms_new_message(FbApi *api, JsonNode *root, GSList *msgs, GError **error) +{ + const gchar *body; + const gchar *str; + GError *err = NULL; + FbApiPrivate *priv = api->priv; + FbApiMessage *dmsg; + FbApiMessage msg; + FbId id; + FbId oid; + FbJsonValues *values; + JsonNode *node; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.offlineThreadingId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.actorFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata" + ".threadKey.otherUserFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata" + ".threadKey.threadFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.timestamp"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.body"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.stickerId"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.messageMetadata.messageId"); + + if (fb_json_values_update(values, &err)) { + id = fb_json_values_next_int(values, 0); + + /* Ignore everything but new messages */ + if (id == 0) { + goto beach; + } + + /* Ignore sequential duplicates */ + if (id == priv->lastmid) { + fb_util_debug_info("Ignoring duplicate %" FB_ID_FORMAT, id); + goto beach; + } + + priv->lastmid = id; + fb_api_message_reset(&msg, FALSE); + msg.uid = fb_json_values_next_int(values, 0); + oid = fb_json_values_next_int(values, 0); + msg.tid = fb_json_values_next_int(values, 0); + msg.tstamp = fb_json_values_next_int(values, 0); + + if (msg.uid == priv->uid) { + msg.flags |= FB_API_MESSAGE_FLAG_SELF; + + if (msg.tid == 0) { + msg.uid = oid; + } + } + + body = fb_json_values_next_str(values, NULL); + + if (body != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + dmsg->text = g_strdup(body); + msgs = g_slist_prepend(msgs, dmsg); + } + + id = fb_json_values_next_int(values, 0); + + if (id != 0) { + dmsg = fb_api_message_dup(&msg, FALSE); + fb_api_sticker(api, id, dmsg); + } + + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + goto beach; + } + + node = fb_json_values_get_root(values); + msgs = fb_api_message_parse_attach(api, str, &msg, msgs, body, + node, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + goto beach; + } + } + +beach: + g_object_unref(values); + return msgs; +} + +static GSList * +fb_api_cb_publish_ms_event(FbApi *api, JsonNode *root, GSList *events, FbApiEventType type, GError **error) +{ + FbApiEvent *event; + FbJsonValues *values = NULL; + FbJsonValues *values_inner = NULL; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.threadKey.threadFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.actorFbId"); + + switch (type) { + case FB_API_EVENT_TYPE_THREAD_TOPIC: + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.name"); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_ADDED: + values_inner = fb_json_values_new(root); + + fb_json_values_add(values_inner, FB_JSON_TYPE_INT, FALSE, + "$.userFbId"); + + /* use the text field for the full name */ + fb_json_values_add(values_inner, FB_JSON_TYPE_STR, FALSE, + "$.fullName"); + + fb_json_values_set_array(values_inner, FALSE, + "$.addedParticipants"); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_REMOVED: + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.leftParticipantFbId"); + + /* use the text field for the kick message */ + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.messageMetadata.adminText"); + break; + } + + fb_json_values_update(values, &err); + + event = fb_api_event_dup(NULL, FALSE); + event->type = type; + event->tid = fb_json_values_next_int(values, 0); + event->uid = fb_json_values_next_int(values, 0); + + if (type == FB_API_EVENT_TYPE_THREAD_TOPIC) { + event->text = fb_json_values_next_str_dup(values, NULL); + } else if (type == FB_API_EVENT_TYPE_THREAD_USER_REMOVED) { + /* overwrite actor with subject */ + event->uid = fb_json_values_next_int(values, 0); + event->text = fb_json_values_next_str_dup(values, NULL); + } else if (type == FB_API_EVENT_TYPE_THREAD_USER_ADDED) { + + while (fb_json_values_update(values_inner, &err)) { + FbApiEvent *devent = fb_api_event_dup(event, FALSE); + + devent->uid = fb_json_values_next_int(values_inner, 0); + devent->text = fb_json_values_next_str_dup(values_inner, NULL); + + events = g_slist_prepend(events, devent); + } + fb_api_event_free(event); + event = NULL; + g_object_unref(values_inner); + } + + g_object_unref(values); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + } else if (event) { + events = g_slist_prepend(events, event); + } + + return events; +} + +static void +fb_api_cb_publish_pt(FbThrift *thft, GSList **press, GError **error) +{ + FbApiPresence *pres; + FbThriftType type; + gint16 id; + gint32 i32; + gint64 i64; + guint i; + guint size = 0; + + /* Read identifier string (for Facebook employees) */ + FB_API_TCHK(fb_thrift_read_str(thft, NULL)); + + /* Read the full list boolean field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0)); + FB_API_TCHK(type == FB_THRIFT_TYPE_BOOL); + FB_API_TCHK(id == 1); + FB_API_TCHK(fb_thrift_read_bool(thft, NULL)); + + /* Read the list field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id)); + FB_API_TCHK(type == FB_THRIFT_TYPE_LIST); + FB_API_TCHK(id == 2); + + /* Read the list */ + FB_API_TCHK(fb_thrift_read_list(thft, &type, &size)); + FB_API_TCHK(type == FB_THRIFT_TYPE_STRUCT); + + for (i = 0; i < size; i++) { + /* Read the user identifier field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0)); + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(id == 1); + FB_API_TCHK(fb_thrift_read_i64(thft, &i64)); + + /* Read the active field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id)); + FB_API_TCHK(type == FB_THRIFT_TYPE_I32); + FB_API_TCHK(id == 2); + FB_API_TCHK(fb_thrift_read_i32(thft, &i32)); + + pres = fb_api_presence_dup(NULL); + pres->uid = i64; + pres->active = i32 != 0; + *press = g_slist_prepend(*press, pres); + + fb_util_debug_info("Presence: %" FB_ID_FORMAT " (%d)", + i64, i32 != 0); + + while (id <= 5) { + if (fb_thrift_read_isstop(thft)) { + break; + } + + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id)); + + switch (id) { + case 3: + /* Read the last active timestamp field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + + case 4: + /* Read the active client bits field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I16); + FB_API_TCHK(fb_thrift_read_i16(thft, NULL)); + break; + + case 5: + /* Read the VoIP compatibility bits field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + + case 6: + /* Unknown new field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + + default: + /* Try to read unknown fields as varint */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I16 || + type == FB_THRIFT_TYPE_I32 || + type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + } + } + + /* Read the field stop */ + FB_API_TCHK(fb_thrift_read_stop(thft)); + } + + /* Read the field stop */ + FB_API_TCHK(fb_thrift_read_stop(thft)); +} + +static void +fb_api_cb_publish_p(FbApi *api, GByteArray *pload) +{ + FbThrift *thft; + GError *err = NULL; + GSList *press = NULL; + + thft = fb_thrift_new(pload, 0); + fb_api_cb_publish_pt(thft, &press, &err); + g_object_unref(thft); + + if (G_LIKELY(err == NULL)) { + g_signal_emit_by_name(api, "presences", press); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(press, (GDestroyNotify) fb_api_presence_free); +} + +static void +fb_api_cb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, GByteArray *pload, + gpointer data) +{ + FbApi *api = data; + gboolean comp; + GByteArray *bytes; + GError *err = NULL; + guint i; + + static const struct { + const gchar *topic; + void (*func) (FbApi *api, GByteArray *pload); + } parsers[] = { + {"/mark_thread_response", fb_api_cb_publish_mark}, + {"/mercury", fb_api_cb_publish_mercury}, + {"/orca_typing_notifications", fb_api_cb_publish_typing}, + {"/send_message_response", fb_api_cb_publish_ms_r}, + {"/t_ms", fb_api_cb_publish_ms}, + {"/t_p", fb_api_cb_publish_p} + }; + + comp = fb_util_zlib_test(pload); + + if (G_LIKELY(comp)) { + bytes = fb_util_zlib_inflate(pload, &err); + FB_API_ERROR_EMIT(api, err, return); + } else { + bytes = (GByteArray *) pload; + } + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes, + "Reading message (topic: %s)", + topic); + + for (i = 0; i < G_N_ELEMENTS(parsers); i++) { + if (g_ascii_strcasecmp(topic, parsers[i].topic) == 0) { + parsers[i].func(api, bytes); + break; + } + } + + if (G_LIKELY(comp)) { + g_byte_array_free(bytes, TRUE); + } +} + +FbApi * +fb_api_new(PurpleConnection *gc) +{ + FbApi *api; + FbApiPrivate *priv; + + api = g_object_new(FB_TYPE_API, NULL); + priv = api->priv; + + priv->gc = gc; + priv->mqtt = fb_mqtt_new(gc); + + g_signal_connect(priv->mqtt, + "connect", + G_CALLBACK(fb_api_cb_mqtt_connect), + api); + g_signal_connect(priv->mqtt, + "error", + G_CALLBACK(fb_api_cb_mqtt_error), + api); + g_signal_connect(priv->mqtt, + "open", + G_CALLBACK(fb_api_cb_mqtt_open), + api); + g_signal_connect(priv->mqtt, + "publish", + G_CALLBACK(fb_api_cb_mqtt_publish), + api); + + return api; +} + +void +fb_api_rehash(FbApi *api) +{ + FbApiPrivate *priv; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + if (priv->cid == NULL) { + priv->cid = fb_util_rand_alnum(32); + } + + if (priv->did == NULL) { + priv->did = purple_uuid_random(); + } + + if (priv->mid == 0) { + priv->mid = g_random_int(); + } + + if (strlen(priv->cid) > 20) { + priv->cid = g_realloc_n(priv->cid , 21, sizeof *priv->cid); + priv->cid[20] = 0; + } +} + +gboolean +fb_api_is_invisible(FbApi *api) +{ + FbApiPrivate *priv; + + g_return_val_if_fail(FB_IS_API(api), FALSE); + priv = api->priv; + + return priv->invisible; +} + +void +fb_api_error(FbApi *api, FbApiError error, const gchar *format, ...) +{ + GError *err; + va_list ap; + + g_return_if_fail(FB_IS_API(api)); + + va_start(ap, format); + err = g_error_new_valist(FB_API_ERROR, error, format, ap); + va_end(ap); + + fb_api_error_emit(api, err); +} + +void +fb_api_error_emit(FbApi *api, GError *error) +{ + g_return_if_fail(FB_IS_API(api)); + g_return_if_fail(error != NULL); + + g_signal_emit_by_name(api, "error", error); + g_error_free(error); +} + +static void +fb_api_cb_attach(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbApiMessage *msg; + FbJsonValues *values; + gchar *name; + GError *err = NULL; + GSList *msgs = NULL; + guint i; + JsonNode *root; + + static const gchar *imgexts[] = {".jpg", ".png", ".gif"}; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.filename"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.redirect_uri"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + msg = fb_api_data_take(api, con); + str = fb_json_values_next_str(values, NULL); + name = g_ascii_strdown(str, -1); + + for (i = 0; i < G_N_ELEMENTS(imgexts); i++) { + if (g_str_has_suffix(name, imgexts[i])) { + msg->flags |= FB_API_MESSAGE_FLAG_IMAGE; + break; + } + } + + g_free(name); + msg->text = fb_json_values_next_str_dup(values, NULL); + msgs = g_slist_prepend(msgs, msg); + + g_signal_emit_by_name(api, "messages", msgs); + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_object_unref(values); + json_node_free(root); + +} + +static void +fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg) +{ + FbHttpParams *prms; + PurpleHttpConnection *http; + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "mid", msgid); + fb_http_params_set_strf(prms, "aid", "%" FB_ID_FORMAT, aid); + + http = fb_api_http_req(api, FB_API_URL_ATTACH, "getAttachment", + "messaging.getAttachment", prms, + fb_api_cb_attach); + fb_api_data_set(api, http, msg, (GDestroyNotify) fb_api_message_free); +} + +static void +fb_api_cb_auth(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.access_token"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.uid"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + g_free(priv->token); + priv->token = fb_json_values_next_str_dup(values, NULL); + priv->uid = fb_json_values_next_int(values, 0); + + g_signal_emit_by_name(api, "auth"); + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_auth(FbApi *api, const gchar *user, const gchar *pass) +{ + FbHttpParams *prms; + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "email", user); + fb_http_params_set_str(prms, "password", pass); + fb_api_http_req(api, FB_API_URL_AUTH, "authenticate", "auth.login", + prms, fb_api_cb_auth); +} + +static gchar * +fb_api_user_icon_checksum(gchar *icon) +{ + gchar *csum; + FbHttpParams *prms; + + if (G_UNLIKELY(icon == NULL)) { + return NULL; + } + + prms = fb_http_params_new_parse(icon, TRUE); + csum = fb_http_params_dup_str(prms, "oh", NULL); + fb_http_params_free(prms); + + if (G_UNLIKELY(csum == NULL)) { + /* Revert to the icon URL as the unique checksum */ + csum = g_strdup(icon); + } + + return csum; +} + +static void +fb_api_cb_contact(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbApiUser user; + FbJsonValues *values; + GError *err = NULL; + JsonNode *node; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + + if (node == NULL) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to obtain contact information")); + json_node_free(root); + return; + } + + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.name"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.profile_pic_large.uri"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + fb_api_user_reset(&user, FALSE); + str = fb_json_values_next_str(values, "0"); + user.uid = FB_ID_FROM_STR(str); + user.name = fb_json_values_next_str_dup(values, NULL); + user.icon = fb_json_values_next_str_dup(values, NULL); + + user.csum = fb_api_user_icon_checksum(user.icon); + + g_signal_emit_by_name(api, "contact", &user); + fb_api_user_reset(&user, TRUE); + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_contact(FbApi *api, FbId uid) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, uid); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "1", "true"); + fb_api_http_query(api, FB_API_QUERY_CONTACT, bldr, fb_api_cb_contact); +} + +static GSList * +fb_api_cb_contacts_nodes(FbApi *api, JsonNode *root, GSList *users) +{ + const gchar *str; + FbApiPrivate *priv = api->priv; + FbApiUser *user; + FbId uid; + FbJsonValues *values; + gboolean is_array; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.represented_profile.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.represented_profile.friendship_status"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.structured_name.text"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.hugePictureUrl.uri"); + + is_array = (JSON_NODE_TYPE(root) == JSON_NODE_ARRAY); + + if (is_array) { + fb_json_values_set_array(values, FALSE, "$"); + } + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, "0"); + uid = FB_ID_FROM_STR(str); + str = fb_json_values_next_str(values, NULL); + + if ((!purple_strequal(str, "ARE_FRIENDS") && + (uid != priv->uid)) || (uid == 0)) + { + if (!is_array) { + break; + } + continue; + } + + user = fb_api_user_dup(NULL, FALSE); + user->uid = uid; + user->name = fb_json_values_next_str_dup(values, NULL); + user->icon = fb_json_values_next_str_dup(values, NULL); + + user->csum = fb_api_user_icon_checksum(user->icon); + + users = g_slist_prepend(users, user); + + if (!is_array) { + break; + } + } + + g_object_unref(values); + + return users; +} + +/* base64(contact:<our id>:<their id>:<whatever>) */ +static GSList * +fb_api_cb_contacts_parse_removed(FbApi *api, JsonNode *node, GSList *users) +{ + gsize len; + char **split; + char *decoded = (char *) g_base64_decode(json_node_get_string(node), &len); + + g_return_val_if_fail(decoded[len] == '\0', users); + g_return_val_if_fail(len == strlen(decoded), users); + g_return_val_if_fail(g_str_has_prefix(decoded, "contact:"), users); + + split = g_strsplit_set(decoded, ":", 4); + + g_return_val_if_fail(g_strv_length(split) == 4, users); + + users = g_slist_prepend(users, g_strdup(split[2])); + + g_strfreev(split); + g_free(decoded); + + return users; +} + +static void +fb_api_cb_contacts(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *cursor; + const gchar *delta_cursor; + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + gboolean complete; + gboolean is_delta; + GError *err = NULL; + GList *l; + GSList *users = NULL; + JsonNode *root; + JsonNode *croot; + JsonNode *node; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + croot = fb_json_node_get(root, "$.viewer.messenger_contacts.deltas", NULL); + is_delta = (croot != NULL); + + if (!is_delta) { + croot = fb_json_node_get(root, "$.viewer.messenger_contacts", NULL); + node = fb_json_node_get(croot, "$.nodes", NULL); + users = fb_api_cb_contacts_nodes(api, node, users); + json_node_free(node); + + } else { + GSList *added = NULL; + GSList *removed = NULL; + JsonArray *arr = fb_json_node_get_arr(croot, "$.nodes", NULL); + GList *elms = json_array_get_elements(arr); + + for (l = elms; l != NULL; l = l->next) { + if ((node = fb_json_node_get(l->data, "$.added", NULL))) { + added = fb_api_cb_contacts_nodes(api, node, added); + json_node_free(node); + } + + if ((node = fb_json_node_get(l->data, "$.removed", NULL))) { + removed = fb_api_cb_contacts_parse_removed(api, node, removed); + json_node_free(node); + } + } + + g_signal_emit_by_name(api, "contacts-delta", added, removed); + + g_slist_free_full(added, (GDestroyNotify) fb_api_user_free); + g_slist_free_full(removed, (GDestroyNotify) g_free); + + g_list_free(elms); + json_array_unref(arr); + } + + values = fb_json_values_new(croot); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE, + "$.page_info.has_next_page"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.page_info.delta_cursor"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.page_info.end_cursor"); + fb_json_values_update(values, NULL); + + complete = !fb_json_values_next_bool(values, FALSE); + + delta_cursor = fb_json_values_next_str(values, NULL); + + cursor = fb_json_values_next_str(values, NULL); + + if (G_UNLIKELY(err == NULL)) { + if (is_delta || complete) { + g_free(priv->contacts_delta); + priv->contacts_delta = g_strdup(is_delta ? cursor : delta_cursor); + } + + if (users) { + g_signal_emit_by_name(api, "contacts", users, complete); + } + + if (!complete) { + fb_api_contacts_after(api, cursor); + } + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(users, (GDestroyNotify) fb_api_user_free); + g_object_unref(values); + + json_node_free(croot); + json_node_free(root); +} + +void +fb_api_contacts(FbApi *api) +{ + FbApiPrivate *priv; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + if (priv->contacts_delta) { + fb_api_contacts_delta(api, priv->contacts_delta); + return; + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_str(bldr, NULL, "user"); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "1", G_STRINGIFY(FB_API_CONTACTS_COUNT)); + fb_api_http_query(api, FB_API_QUERY_CONTACTS, bldr, + fb_api_cb_contacts); +} + +static void +fb_api_contacts_after(FbApi *api, const gchar *cursor) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_str(bldr, NULL, "user"); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "1", cursor); + fb_json_bldr_add_str(bldr, "2", G_STRINGIFY(FB_API_CONTACTS_COUNT)); + fb_api_http_query(api, FB_API_QUERY_CONTACTS_AFTER, bldr, + fb_api_cb_contacts); +} + +void +fb_api_contacts_delta(FbApi *api, const gchar *delta_cursor) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + + fb_json_bldr_add_str(bldr, "0", delta_cursor); + + fb_json_bldr_arr_begin(bldr, "1"); + fb_json_bldr_add_str(bldr, NULL, "user"); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "2", G_STRINGIFY(FB_API_CONTACTS_COUNT)); + fb_api_http_query(api, FB_API_QUERY_CONTACTS_DELTA, bldr, + fb_api_cb_contacts); +} + +void +fb_api_connect(FbApi *api, gboolean invisible) +{ + FbApiPrivate *priv; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + priv->invisible = invisible; + fb_mqtt_open(priv->mqtt, FB_MQTT_HOST, FB_MQTT_PORT); +} + +void +fb_api_disconnect(FbApi *api) +{ + FbApiPrivate *priv; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + fb_mqtt_disconnect(priv->mqtt); +} + +static void +fb_api_message_send(FbApi *api, FbApiMessage *msg) +{ + const gchar *tpfx; + FbApiPrivate *priv = api->priv; + FbId id; + FbId mid; + gchar *json; + JsonBuilder *bldr; + + mid = FB_API_MSGID(g_get_real_time() / 1000, g_random_int()); + priv->lastmid = mid; + + if (msg->tid != 0) { + tpfx = "tfbid_"; + id = msg->tid; + } else { + tpfx = ""; + id = msg->uid; + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "body", msg->text); + fb_json_bldr_add_strf(bldr, "msgid", "%" FB_ID_FORMAT, mid); + fb_json_bldr_add_strf(bldr, "sender_fbid", "%" FB_ID_FORMAT, priv->uid); + fb_json_bldr_add_strf(bldr, "to", "%s%" FB_ID_FORMAT, tpfx, id); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/send_message2", "%s", json); + g_free(json); +} + +void +fb_api_message(FbApi *api, FbId id, gboolean thread, const gchar *text) +{ + FbApiMessage *msg; + FbApiPrivate *priv; + gboolean empty; + + g_return_if_fail(FB_IS_API(api)); + g_return_if_fail(text != NULL); + priv = api->priv; + + msg = fb_api_message_dup(NULL, FALSE); + msg->text = g_strdup(text); + + if (thread) { + msg->tid = id; + } else { + msg->uid = id; + } + + empty = g_queue_is_empty(priv->msgs); + g_queue_push_tail(priv->msgs, msg); + + if (empty && fb_mqtt_connected(priv->mqtt, FALSE)) { + fb_api_message_send(api, msg); + } +} + +void +fb_api_publish(FbApi *api, const gchar *topic, const gchar *format, ...) +{ + FbApiPrivate *priv; + GByteArray *bytes; + GByteArray *cytes; + gchar *msg; + GError *err = NULL; + va_list ap; + + g_return_if_fail(FB_IS_API(api)); + g_return_if_fail(topic != NULL); + g_return_if_fail(format != NULL); + priv = api->priv; + + va_start(ap, format); + msg = g_strdup_vprintf(format, ap); + va_end(ap); + + bytes = g_byte_array_new_take((guint8 *) msg, strlen(msg)); + cytes = fb_util_zlib_deflate(bytes, &err); + + FB_API_ERROR_EMIT(api, err, + g_byte_array_free(bytes, TRUE); + return; + ); + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes, + "Writing message (topic: %s)", + topic); + + fb_mqtt_publish(priv->mqtt, topic, cytes); + g_byte_array_free(cytes, TRUE); + g_byte_array_free(bytes, TRUE); +} + +void +fb_api_read(FbApi *api, FbId id, gboolean thread) +{ + const gchar *key; + FbApiPrivate *priv; + gchar *json; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_bool(bldr, "state", TRUE); + fb_json_bldr_add_int(bldr, "syncSeqId", priv->sid); + fb_json_bldr_add_str(bldr, "mark", "read"); + + key = thread ? "threadFbId" : "otherUserFbId"; + fb_json_bldr_add_strf(bldr, key, "%" FB_ID_FORMAT, id); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/mark_thread", "%s", json); + g_free(json); +} + +static GSList * +fb_api_cb_unread_parse_attach(FbApi *api, const gchar *mid, FbApiMessage *msg, + GSList *msgs, JsonNode *root, GError **error) +{ + const gchar *str; + FbApiMessage *dmsg; + FbId id; + FbJsonValues *values; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.attachment_fbid"); + fb_json_values_set_array(values, FALSE, "$.blob_attachments"); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, NULL); + id = FB_ID_FROM_STR(str); + dmsg = fb_api_message_dup(msg, FALSE); + fb_api_attach(api, id, mid, dmsg); + } + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + } + + g_object_unref(values); + return msgs; +} + +static void +fb_api_cb_unread_msgs(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *body; + const gchar *str; + FbApi *api = data; + FbApiMessage *dmsg; + FbApiMessage msg; + FbId id; + FbId tid; + FbJsonValues *values; + gchar *xma; + GError *err = NULL; + GSList *msgs = NULL; + JsonNode *node; + JsonNode *root; + JsonNode *xode; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + + if (node == NULL) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to obtain unread messages")); + json_node_free(root); + return; + } + + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.thread_fbid"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + return; + ); + + fb_api_message_reset(&msg, FALSE); + str = fb_json_values_next_str(values, "0"); + tid = FB_ID_FROM_STR(str); + g_object_unref(values); + + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, TRUE, "$.unread"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.message_sender.messaging_actor.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.message.text"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.timestamp_precise"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.sticker.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.message_id"); + fb_json_values_set_array(values, FALSE, "$.messages.nodes"); + + while (fb_json_values_update(values, &err)) { + if (!fb_json_values_next_bool(values, FALSE)) { + continue; + } + + str = fb_json_values_next_str(values, "0"); + body = fb_json_values_next_str(values, NULL); + + fb_api_message_reset(&msg, FALSE); + msg.uid = FB_ID_FROM_STR(str); + msg.tid = tid; + + str = fb_json_values_next_str(values, "0"); + msg.tstamp = g_ascii_strtoll(str, NULL, 10); + + if (body != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + dmsg->text = g_strdup(body); + msgs = g_slist_prepend(msgs, dmsg); + } + + str = fb_json_values_next_str(values, NULL); + + if (str != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + id = FB_ID_FROM_STR(str); + fb_api_sticker(api, id, dmsg); + } + + node = fb_json_values_get_root(values); + xode = fb_json_node_get(node, "$.extensible_attachment", NULL); + + if (xode != NULL) { + xma = fb_api_xma_parse(api, body, xode, &err); + + if (xma != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + dmsg->text = xma; + msgs = g_slist_prepend(msgs, dmsg); + } + + json_node_free(xode); + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + continue; + } + + msgs = fb_api_cb_unread_parse_attach(api, str, &msg, msgs, + node, &err); + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + if (G_UNLIKELY(err == NULL)) { + msgs = g_slist_reverse(msgs); + g_signal_emit_by_name(api, "messages", msgs); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_cb_unread(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *id; + FbApi *api = data; + FbJsonValues *values; + GError *err = NULL; + gint64 count; + JsonBuilder *bldr; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.unread_count"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.other_user_id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.thread_fbid"); + fb_json_values_set_array(values, FALSE, "$.viewer.message_threads" + ".nodes"); + + while (fb_json_values_update(values, &err)) { + count = fb_json_values_next_int(values, -5); + + if (count < 1) { + continue; + } + + id = fb_json_values_next_str(values, NULL); + + if (id == NULL) { + id = fb_json_values_next_str(values, "0"); + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_str(bldr, NULL, id); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "10", "true"); + fb_json_bldr_add_str(bldr, "11", "true"); + fb_json_bldr_add_int(bldr, "12", count); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREAD, bldr, + fb_api_cb_unread_msgs); + } + + if (G_UNLIKELY(err != NULL)) { + fb_api_error_emit(api, err); + } + + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_unread(FbApi *api) +{ + FbApiPrivate *priv; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + if (priv->unread < 1) { + return; + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "2", "true"); + fb_json_bldr_add_int(bldr, "1", priv->unread); + fb_json_bldr_add_str(bldr, "12", "true"); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREADS, bldr, + fb_api_cb_unread); +} + +static void +fb_api_cb_sticker(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiMessage *msg; + FbJsonValues *values; + GError *err = NULL; + GSList *msgs = NULL; + JsonNode *node; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.thread_image.uri"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + msg = fb_api_data_take(api, con); + msg->flags |= FB_API_MESSAGE_FLAG_IMAGE; + msg->text = fb_json_values_next_str_dup(values, NULL); + msgs = g_slist_prepend(msgs, msg); + + g_signal_emit_by_name(api, "messages", msgs); + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_sticker(FbApi *api, FbId sid, FbApiMessage *msg) +{ + JsonBuilder *bldr; + PurpleHttpConnection *http; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, sid); + fb_json_bldr_arr_end(bldr); + + http = fb_api_http_query(api, FB_API_QUERY_STICKER, bldr, + fb_api_cb_sticker); + fb_api_data_set(api, http, msg, (GDestroyNotify) fb_api_message_free); +} + +static gboolean +fb_api_thread_parse(FbApi *api, FbApiThread *thrd, JsonNode *root, + GError **error) +{ + const gchar *str; + FbApiPrivate *priv = api->priv; + FbApiUser *user; + FbId uid; + FbJsonValues *values; + gboolean haself = FALSE; + guint num_users = 0; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.thread_fbid"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.name"); + fb_json_values_update(values, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + g_object_unref(values); + return FALSE; + } + + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + g_object_unref(values); + return FALSE; + } + + thrd->tid = FB_ID_FROM_STR(str); + thrd->topic = fb_json_values_next_str_dup(values, NULL); + g_object_unref(values); + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.messaging_actor.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.messaging_actor.name"); + fb_json_values_set_array(values, TRUE, "$.all_participants.nodes"); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, "0"); + uid = FB_ID_FROM_STR(str); + num_users++; + + if (uid != priv->uid) { + user = fb_api_user_dup(NULL, FALSE); + user->uid = uid; + user->name = fb_json_values_next_str_dup(values, NULL); + thrd->users = g_slist_prepend(thrd->users, user); + } else { + haself = TRUE; + } + } + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + fb_api_thread_reset(thrd, TRUE); + g_object_unref(values); + return FALSE; + } + + if (num_users < 2 || !haself) { + g_object_unref(values); + return FALSE; + } + + g_object_unref(values); + return TRUE; +} + +static void +fb_api_cb_thread(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiThread thrd; + GError *err = NULL; + JsonNode *node; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + + if (node == NULL) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to obtain thread information")); + json_node_free(root); + return; + } + + fb_api_thread_reset(&thrd, FALSE); + + if (!fb_api_thread_parse(api, &thrd, node, &err)) { + if (G_LIKELY(err == NULL)) { + if (thrd.tid) { + g_signal_emit_by_name(api, "thread-kicked", &thrd); + } else { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to parse thread information")); + } + } else { + fb_api_error_emit(api, err); + } + } else { + g_signal_emit_by_name(api, "thread", &thrd); + } + + fb_api_thread_reset(&thrd, TRUE); + json_node_free(root); +} + +void +fb_api_thread(FbApi *api, FbId tid) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, tid); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "10", "false"); + fb_json_bldr_add_str(bldr, "11", "false"); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREAD, bldr, fb_api_cb_thread); +} + +static void +fb_api_cb_thread_create(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbId tid; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.id"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + str = fb_json_values_next_str(values, "0"); + tid = FB_ID_FROM_STR(str); + g_signal_emit_by_name(api, "thread-create", tid); + + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_thread_create(FbApi *api, GSList *uids) +{ + FbApiPrivate *priv; + FbHttpParams *prms; + FbId *uid; + gchar *json; + GSList *l; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + g_warn_if_fail(g_slist_length(uids) > 1); + priv = api->priv; + + bldr = fb_json_bldr_new(JSON_NODE_ARRAY); + fb_json_bldr_obj_begin(bldr, NULL); + fb_json_bldr_add_str(bldr, "type", "id"); + fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, priv->uid); + fb_json_bldr_obj_end(bldr); + + for (l = uids; l != NULL; l = l->next) { + uid = l->data; + fb_json_bldr_obj_begin(bldr, NULL); + fb_json_bldr_add_str(bldr, "type", "id"); + fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, *uid); + fb_json_bldr_obj_end(bldr); + } + + json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL); + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "recipients", json); + fb_api_http_req(api, FB_API_URL_THREADS, "createGroup", "POST", + prms, fb_api_cb_thread_create); + g_free(json); +} + +void +fb_api_thread_invite(FbApi *api, FbId tid, FbId uid) +{ + FbHttpParams *prms; + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_ARRAY); + fb_json_bldr_obj_begin(bldr, NULL); + fb_json_bldr_add_str(bldr, "type", "id"); + fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, uid); + fb_json_bldr_obj_end(bldr); + json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL); + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "to", json); + fb_http_params_set_strf(prms, "id", "t_%" FB_ID_FORMAT, tid); + fb_api_http_req(api, FB_API_URL_PARTS, "addMembers", "POST", + prms, fb_api_cb_http_bool); + g_free(json); +} + +void +fb_api_thread_remove(FbApi *api, FbId tid, FbId uid) +{ + FbApiPrivate *priv; + FbHttpParams *prms; + gchar *json; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + prms = fb_http_params_new(); + fb_http_params_set_strf(prms, "id", "t_%" FB_ID_FORMAT, tid); + + if (uid == 0) { + uid = priv->uid; + } + + if (uid != priv->uid) { + bldr = fb_json_bldr_new(JSON_NODE_ARRAY); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, uid); + json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL); + fb_http_params_set_str(prms, "to", json); + g_free(json); + } + + fb_api_http_req(api, FB_API_URL_PARTS, "removeMembers", "DELETE", + prms, fb_api_cb_http_bool); +} + +void +fb_api_thread_topic(FbApi *api, FbId tid, const gchar *topic) +{ + FbHttpParams *prms; + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "name", topic); + fb_http_params_set_int(prms, "tid", tid); + fb_api_http_req(api, FB_API_URL_TOPIC, "setThreadName", + "messaging.setthreadname", prms, + fb_api_cb_http_bool); +} + +static void +fb_api_cb_threads(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiThread *dthrd; + FbApiThread thrd; + GError *err = NULL; + GList *elms; + GList *l; + GSList *thrds = NULL; + JsonArray *arr; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + arr = fb_json_node_get_arr(root, "$.viewer.message_threads.nodes", + &err); + FB_API_ERROR_EMIT(api, err, + json_node_free(root); + return; + ); + + elms = json_array_get_elements(arr); + + for (l = elms; l != NULL; l = l->next) { + fb_api_thread_reset(&thrd, FALSE); + + if (fb_api_thread_parse(api, &thrd, l->data, &err)) { + dthrd = fb_api_thread_dup(&thrd, FALSE); + thrds = g_slist_prepend(thrds, dthrd); + } else { + fb_api_thread_reset(&thrd, TRUE); + } + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + if (G_LIKELY(err == NULL)) { + thrds = g_slist_reverse(thrds); + g_signal_emit_by_name(api, "threads", thrds); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(thrds, (GDestroyNotify) fb_api_thread_free); + g_list_free(elms); + json_array_unref(arr); + json_node_free(root); +} + +void +fb_api_threads(FbApi *api) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "2", "true"); + fb_json_bldr_add_str(bldr, "12", "false"); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREADS, bldr, fb_api_cb_threads); +} + +void +fb_api_typing(FbApi *api, FbId uid, gboolean state) +{ + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_int(bldr, "state", state != 0); + fb_json_bldr_add_strf(bldr, "to", "%" FB_ID_FORMAT, uid); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/typing", "%s", json); + g_free(json); +} + +FbApiEvent * +fb_api_event_dup(const FbApiEvent *event, gboolean deep) +{ + FbApiEvent *ret; + + if (event == NULL) { + return g_new0(FbApiEvent, 1); + } + + ret = g_memdup(event, sizeof *event); + + if (deep) { + ret->text = g_strdup(event->text); + } + + return ret; +} + +void +fb_api_event_reset(FbApiEvent *event, gboolean deep) +{ + g_return_if_fail(event != NULL); + + if (deep) { + g_free(event->text); + } + + memset(event, 0, sizeof *event); +} + +void +fb_api_event_free(FbApiEvent *event) +{ + if (G_LIKELY(event != NULL)) { + g_free(event->text); + g_free(event); + } +} + +FbApiMessage * +fb_api_message_dup(const FbApiMessage *msg, gboolean deep) +{ + FbApiMessage *ret; + + if (msg == NULL) { + return g_new0(FbApiMessage, 1); + } + + ret = g_memdup(msg, sizeof *msg); + + if (deep) { + ret->text = g_strdup(msg->text); + } + + return ret; +} + +void +fb_api_message_reset(FbApiMessage *msg, gboolean deep) +{ + g_return_if_fail(msg != NULL); + + if (deep) { + g_free(msg->text); + } + + memset(msg, 0, sizeof *msg); +} + +void +fb_api_message_free(FbApiMessage *msg) +{ + if (G_LIKELY(msg != NULL)) { + g_free(msg->text); + g_free(msg); + } +} + +FbApiPresence * +fb_api_presence_dup(const FbApiPresence *pres) +{ + if (pres == NULL) { + return g_new0(FbApiPresence, 1); + } + + return g_memdup(pres, sizeof *pres); +} + +void +fb_api_presence_reset(FbApiPresence *pres) +{ + g_return_if_fail(pres != NULL); + memset(pres, 0, sizeof *pres); +} + +void +fb_api_presence_free(FbApiPresence *pres) +{ + if (G_LIKELY(pres != NULL)) { + g_free(pres); + } +} + +FbApiThread * +fb_api_thread_dup(const FbApiThread *thrd, gboolean deep) +{ + FbApiThread *ret; + FbApiUser *user; + GSList *l; + + if (thrd == NULL) { + return g_new0(FbApiThread, 1); + } + + ret = g_memdup(thrd, sizeof *thrd); + + if (deep) { + ret->users = NULL; + + for (l = thrd->users; l != NULL; l = l->next) { + user = fb_api_user_dup(l->data, TRUE); + ret->users = g_slist_prepend(ret->users, user); + } + + ret->topic = g_strdup(thrd->topic); + ret->users = g_slist_reverse(ret->users); + } + + return ret; +} + +void +fb_api_thread_reset(FbApiThread *thrd, gboolean deep) +{ + g_return_if_fail(thrd != NULL); + + if (deep) { + g_slist_free_full(thrd->users, (GDestroyNotify) fb_api_user_free); + g_free(thrd->topic); + } + + memset(thrd, 0, sizeof *thrd); +} + +void +fb_api_thread_free(FbApiThread *thrd) +{ + if (G_LIKELY(thrd != NULL)) { + g_slist_free_full(thrd->users, (GDestroyNotify) fb_api_user_free); + g_free(thrd->topic); + g_free(thrd); + } +} + +FbApiTyping * +fb_api_typing_dup(const FbApiTyping *typg) +{ + if (typg == NULL) { + return g_new0(FbApiTyping, 1); + } + + return g_memdup(typg, sizeof *typg); +} + +void +fb_api_typing_reset(FbApiTyping *typg) +{ + g_return_if_fail(typg != NULL); + memset(typg, 0, sizeof *typg); +} + +void +fb_api_typing_free(FbApiTyping *typg) +{ + if (G_LIKELY(typg != NULL)) { + g_free(typg); + } +} + +FbApiUser * +fb_api_user_dup(const FbApiUser *user, gboolean deep) +{ + FbApiUser *ret; + + if (user == NULL) { + return g_new0(FbApiUser, 1); + } + + ret = g_memdup(user, sizeof *user); + + if (deep) { + ret->name = g_strdup(user->name); + ret->icon = g_strdup(user->icon); + ret->csum = g_strdup(user->csum); + } + + return ret; +} + +void +fb_api_user_reset(FbApiUser *user, gboolean deep) +{ + g_return_if_fail(user != NULL); + + if (deep) { + g_free(user->name); + g_free(user->icon); + g_free(user->csum); + } + + memset(user, 0, sizeof *user); +} + +void +fb_api_user_free(FbApiUser *user) +{ + if (G_LIKELY(user != NULL)) { + g_free(user->name); + g_free(user->icon); + g_free(user->csum); + g_free(user); + } +} diff --git a/pidgin/libpurple/protocols/facebook/api.h b/pidgin/libpurple/protocols/facebook/api.h new file mode 100644 index 0000000..111788e --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/api.h @@ -0,0 +1,1006 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_API_H_ +#define _FACEBOOK_API_H_ + +/** + * SECTION:api + * @section_id: facebook-api + * @short_description: <filename>api.h</filename> + * @title: Facebook API + * + * The API for interacting with the Facebook Messenger protocol. + */ + +#include "internal.h" + +#include <glib.h> + +#include "connection.h" + +#include "http.h" +#include "id.h" +#include "mqtt.h" + +#define FB_TYPE_API (fb_api_get_type()) +#define FB_API(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_API, FbApi)) +#define FB_API_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_API, FbApiClass)) +#define FB_IS_API(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_API)) +#define FB_IS_API_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_API)) +#define FB_API_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_API, FbApiClass)) + +/** + * FB_API_AHOST: + * + * The HTTP host for the Facebook API. + */ +#define FB_API_AHOST "https://api.facebook.com" + +/** + * FB_API_BHOST: + * + * The HTTP host for the Facebook BAPI. + */ +#define FB_API_BHOST "https://b-api.facebook.com" + +/** + * FB_API_GHOST: + * + * The HTTP host for the Facebook Graph API. + */ +#define FB_API_GHOST "https://graph.facebook.com" + +/** + * FB_API_WHOST: + * + * The HTTP host for the Facebook website. + */ +#define FB_API_WHOST "https://www.facebook.com" + +/** + * FB_API_FBRPC_PREFIX + * + * The fbrpc URL prefix used in links shared from the mobile app. + */ +#define FB_API_FBRPC_PREFIX "fbrpc://facebook/nativethirdparty" + +/** + * FB_API_KEY: + * + * The Facebook API key. + */ +#define FB_API_KEY "256002347743983" + +/** + * FB_API_SECRET: + * + * The Facebook API secret. + */ +#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895" + +/** + * FB_ORCA_AGENT + * + * The part of the user agent that looks like the official client, since the + * server started checking this. + */ + +#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/109.0.0.17.70;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" + +/** + * FB_API_AGENT: + * + * The HTTP User-Agent header. + */ +#define FB_API_AGENT "Facebook plugin / Purple / 0.9.5 " FB_ORCA_AGENT + +/** + * FB_API_MQTT_AGENT + * + * The client information string sent in the MQTT CONNECT message + */ + +#define FB_API_MQTT_AGENT FB_API_AGENT + +/** + * FB_API_URL_ATTACH: + * + * The URL for attachment URL requests. + */ +#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.getAttachment" +//#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.attachmentRedirect" + +/** + * FB_API_URL_AUTH: + * + * The URL for authentication requests. + */ +#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login" + +/** + * FB_API_URL_GQL: + * + * The URL for GraphQL requests. + */ +#define FB_API_URL_GQL FB_API_GHOST "/graphql" + +/** + * FB_API_URL_MESSAGES: + * + * The URL for linking message threads. + */ +#define FB_API_URL_MESSAGES FB_API_WHOST "/messages" + +/** + * FB_API_URL_PARTS: + * + * The URL for participant management requests. + */ +#define FB_API_URL_PARTS FB_API_GHOST "/participants" + +/** + * FB_API_URL_THREADS: + * + * The URL for thread management requests. + */ +#define FB_API_URL_THREADS FB_API_GHOST "/me/group_threads" + +/** + * FB_API_URL_TOPIC: + * + * The URL for thread topic requests. + */ +#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname" + +/** + * FB_API_QUERY_CONTACT: + * + * The query hash for the `UsersQuery`. + * + * Key mapping: + * 0: user_fbids + * 1: include_full_user_info + * 2: profile_pic_large_size + * 3: profile_pic_medium_size + * 4: profile_pic_small_size + */ +#define FB_API_QUERY_CONTACT 10153915107411729 + +/** + * FB_API_QUERY_CONTACTS: + * + * The query hash for the `FetchContactsFullQuery`. + * + * Key mapping: + * 0: profile_types + * 1: limit + * 2: big_img_size + * 3: huge_img_size + * 4: small_img_size + */ +#define FB_API_QUERY_CONTACTS 10154444360806729 + +/** + * FB_API_QUERY_CONTACTS_AFTER: + * + * The query hash for the `FetchContactsFullWithAfterQuery`. + * + * Key mapping: + * 0: profile_types + * 1: after + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_AFTER 10154444360816729 + + +/** + * FB_API_QUERY_CONTACTS_DELTA: + * + * The query hash for the `FetchContactsDeltaQuery`. + * + * Key mapping: + * 0: after + * 1: profile_types + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_DELTA 10154444360801729 + +/** + * FB_API_QUERY_STICKER: + * + * The query hash for the `FetchStickersWithPreviewsQuery`. + * + * Key mapping: + * 0: sticker_ids + * 1: media_type + * 2: preview_size + * 3: scaling_factor + * 4: animated_media_type + */ +#define FB_API_QUERY_STICKER 10152877994321729 + +/** + * FB_API_QUERY_THREAD: + * + * The query hash for the `ThreadQuery`. + * + * Key mapping: + * 0: thread_ids + * 1: verification_type + * 2: hash_key + * 3: small_preview_size + * 4: large_preview_size + * 5: item_count + * 6: event_count + * 7: full_screen_height + * 8: full_screen_width + * 9: medium_preview_size + * 10: fetch_users_separately + * 11: include_message_info + * 12: msg_count + * 13: include_full_user_info + * 14: profile_pic_large_size + * 15: profile_pic_medium_size + * 16: profile_pic_small_size + */ +#define FB_API_QUERY_THREAD 10153919752036729 + +/** + * FB_API_QUERY_THREADS: + * + * The query hash for the `ThreadListQuery`. + * + * Key mapping: + * 0: folder_tag + * 1: thread_count + * 2: include_thread_info + * 3: verification_type + * 4: hash_key + * 5: small_preview_size + * 6: large_preview_size + * 7: item_count + * 8: event_count + * 9: full_screen_height + * 10: full_screen_width + * 11: medium_preview_size + * 12: fetch_users_separately + * 13: include_message_info + * 14: msg_count + * 15: UNKNOWN + * 16: profile_pic_large_size + * 17: profile_pic_medium_size + * 18: profile_pic_small_size + */ +#define FB_API_QUERY_THREADS 10153919752026729 + +/** + * FB_API_QUERY_SEQ_ID: + * + * A variant of ThreadListQuery with sequence ID + * + * TODO: parameters. + */ + +#define FB_API_QUERY_SEQ_ID 10155268192741729 + +/** + * FB_API_QUERY_XMA: + * + * The query hash for the `XMAQuery`. + * + * Key mapping: + * 0: xma_id + */ +#define FB_API_QUERY_XMA 10153919431161729 + +/** + * FB_API_CONTACTS_COUNT: + * + * The maximum amount of contacts to fetch in a single request. If this + * value is set too high, HTTP request will fail. This is due to the + * request data being too large. + */ +#define FB_API_CONTACTS_COUNT 500 + +/** + * FB_API_TCHK: + * @e: The expression. + * + * Checks the Thrift related expression to ensure that it evaluates to + * #TRUE. If the expression evaluates to #FALSE, a #GError is assigned + * to the local `error` variable, then returns with no value. + * + * This macro is meant to only be used for Thrift related expressions, + * where the calling function has a `void` return type. This macro also + * requires the existence of a predefined `error` variable, which is a + * pointer of a pointer to a #GError. + */ +#define FB_API_TCHK(e) \ + G_STMT_START { \ + if (G_UNLIKELY(!(e))) { \ + g_set_error(error, FB_API_ERROR, FB_API_ERROR_GENERAL, \ + "Failed to read thrift: %s:%d " \ + "%s: assertion '%s' failed", \ + __FILE__, __LINE__, G_STRFUNC, #e); \ + return; \ + } \ + } G_STMT_END + +/** + * FB_API_MSGID: + * @m: The time in milliseconds. + * @i: The random integer. + * + * Creates a 64-bit message identifier in the Facebook format. + * + * Returns: The message identifier. + */ +#define FB_API_MSGID(m, i) ((guint64) ( \ + (((guint32) i) & 0x3FFFFF) | \ + (((guint64) m) << 22) \ + )) + +/** + * FB_API_ERROR_EMIT: + * @a: The #FbApi. + * @e: The #FbApiError. + * @c: The code to execute. + * + * Emits a #GError on behalf of the #FbApi. + */ +#define FB_API_ERROR_EMIT(a, e, c) \ + G_STMT_START { \ + if (G_UNLIKELY((e) != NULL)) { \ + fb_api_error_emit(a, e); \ + {c;} \ + } \ + } G_STMT_END + +/** + * FB_API_ERROR: + * + * The #GQuark of the domain of API errors. + */ +#define FB_API_ERROR fb_api_error_quark() + +typedef struct _FbApi FbApi; +typedef struct _FbApiClass FbApiClass; +typedef struct _FbApiPrivate FbApiPrivate; +typedef struct _FbApiEvent FbApiEvent; +typedef struct _FbApiMessage FbApiMessage; +typedef struct _FbApiPresence FbApiPresence; +typedef struct _FbApiThread FbApiThread; +typedef struct _FbApiTyping FbApiTyping; +typedef struct _FbApiUser FbApiUser; + +/** + * FbApiError: + * @FB_API_ERROR_GENERAL: General failure. + * @FB_API_ERROR_AUTH: Authentication failure. + * @FB_API_ERROR_QUEUE: Queue failure. + * @FB_API_ERROR_NONFATAL: Other non-fatal errors. + * + * The error codes for the #FB_API_ERROR domain. + */ +typedef enum +{ + FB_API_ERROR_GENERAL, + FB_API_ERROR_AUTH, + FB_API_ERROR_QUEUE, + FB_API_ERROR_NONFATAL +} FbApiError; + +/** + * FbApiEventType: + * @FB_API_EVENT_TYPE_THREAD_TOPIC: The thread topic was changed. + * @FB_API_EVENT_TYPE_THREAD_USER_ADDED: A thread user was added. + * @FB_API_EVENT_TYPE_THREAD_USER_REMOVED: A thread user was removed. + * + * The #FbApiEvent types. + */ +typedef enum +{ + FB_API_EVENT_TYPE_THREAD_TOPIC, + FB_API_EVENT_TYPE_THREAD_USER_ADDED, + FB_API_EVENT_TYPE_THREAD_USER_REMOVED +} FbApiEventType; + +/** + * FbApiMessageFlags: + * @FB_API_MESSAGE_FLAG_DONE: The text has been processed. + * @FB_API_MESSAGE_FLAG_IMAGE: The text is a URL to an image. + * @FB_API_MESSAGE_FLAG_SELF: The text is from the #FbApi user. + * + * The #FbApiMessage flags. + */ +typedef enum +{ + FB_API_MESSAGE_FLAG_DONE = 1 << 0, + FB_API_MESSAGE_FLAG_IMAGE = 1 << 1, + FB_API_MESSAGE_FLAG_SELF = 1 << 2 +} FbApiMessageFlags; + +/** + * FbApi: + * + * Represents a Facebook Messenger connection. + */ +struct _FbApi +{ + /*< private >*/ + GObject parent; + FbApiPrivate *priv; +}; + +/** + * FbApiClass: + * + * The base class for all #FbApi's. + */ +struct _FbApiClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * FbApiEvent: + * @type: The #FbApiEventType. + * @uid: The user #FbId. + * @tid: The thread #FbId. + * @text: The event text. + * + * Represents a Facebook update event. + */ +struct _FbApiEvent +{ + FbApiEventType type; + FbId uid; + FbId tid; + gchar *text; +}; + +/** + * FbApiMessage: + * @flags: The #FbApiMessageFlags. + * @uid: The user #FbId. + * @tid: The thread #FbId. + * @tstamp: The timestamp in milliseconds (UTC). + * @text: The message text. + * + * Represents a Facebook user message. + */ +struct _FbApiMessage +{ + FbApiMessageFlags flags; + FbId uid; + FbId tid; + gint64 tstamp; + gchar *text; +}; + +/** + * FbApiPresence: + * @uid: The user #FbId. + * @active: #TRUE if the user is active, otherwise #FALSE. + * + * Represents a Facebook presence message. + */ +struct _FbApiPresence +{ + FbId uid; + gboolean active; +}; + +/** + * FbApiThread: + * @tid: The thread #FbId. + * @topic: The topic. + * @users: The #GSList of #FbApiUser's. + * + * Represents a Facebook message thread. + */ +struct _FbApiThread +{ + FbId tid; + gchar *topic; + GSList *users; +}; + +/** + * FbApiTyping: + * @uid: The user #FbId. + * @state: #TRUE if the user is typing, otherwise #FALSE. + * + * Represents a Facebook typing message. + */ +struct _FbApiTyping +{ + FbId uid; + gboolean state; +}; + +/** + * FbApiUser: + * @uid: The user #FbId. + * @name: The name of the user. + * @icon: The icon URL. + * @csum: The checksum of @icon. + * + * Represents a Facebook user. + */ +struct _FbApiUser +{ + FbId uid; + gchar *name; + gchar *icon; + gchar *csum; +}; + +/** + * fb_api_get_type: + * + * Returns: The #GType for an #FbApi. + */ +GType +fb_api_get_type(void); + +/** + * fb_api_error_quark: + * + * Gets the #GQuark of the domain of API errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_api_error_quark(void); + +/** + * fb_api_new: + * @gc: The #PurpleConnection. + * + * Creates a new #FbApi. The returned #FbApi should be freed with + * #g_object_unref() when no longer needed. + * + * Returns: The new #FbApi. + */ +FbApi * +fb_api_new(PurpleConnection *gc); + +/** + * fb_api_rehash: + * @api: The #FbApi. + * + * Rehashes and updates internal data of the #FbApi. This should be + * called whenever properties are modified. + */ +void +fb_api_rehash(FbApi *api); + +/** + * fb_api_is_invisible: + * @api: The #FbApi. + * + * Determines if the user of the #FbApi is invisible. + * + * Returns: #TRUE if the #FbApi user is invisible, otherwise #FALSE. + */ +gboolean +fb_api_is_invisible(FbApi *api); + +/** + * fb_api_error: + * @api: The #FbApi. + * @error: The #FbApiError. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Emits an #FbApiError. + */ +void +fb_api_error(FbApi *api, FbApiError error, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_api_error_emit: + * @api: The #FbApi. + * @error: The #GError. + * + * Emits a #GError on an #FbApiError. + */ +void +fb_api_error_emit(FbApi *api, GError *error); + +/** + * fb_api_auth: + * @api: The #FbApi. + * @user: The Facebook user name, email, or phone number. + * @pass: The Facebook password. + * + * Sends an authentication request to Facebook. This will obtain + * session information, which is required for all other requests. + */ +void +fb_api_auth(FbApi *api, const gchar *user, const gchar *pass); + +/** + * fb_api_contact: + * @api: The #FbApi. + * @uid: The user #FbId. + * + * Sends a contact request. This will obtain the general information of + * a single contact. + */ +void +fb_api_contact(FbApi *api, FbId uid); + +/** + * fb_api_contacts: + * @api: The #FbApi. + * + * Sends a contacts request. This will obtain a full list of detailed + * contact information about the friends of the #FbApi user. + */ +void +fb_api_contacts(FbApi *api); + +/** + * fb_api_connect: + * @api: The #FbApi. + * @invisible: #TRUE to make the user invisible, otherwise #FALSE. + * + * Initializes and establishes the underlying MQTT connection. + */ +void +fb_api_connect(FbApi *api, gboolean invisible); + +/** + * fb_api_disconnect: + * @api: The #FbApi. + * + * Closes the underlying MQTT connection. + */ +void +fb_api_disconnect(FbApi *api); + +/** + * fb_api_message: + * @api: The #FbApi. + * @id: The user or thread #FbId. + * @thread: #TRUE if @id is a thread, otherwise #FALSE. + * @text: The message text. + * + * Sends a message as the user of the #FbApi to a user or a thread. + */ +void +fb_api_message(FbApi *api, FbId id, gboolean thread, const gchar *text); + +/** + * fb_api_publish: + * @api: The #FbApi. + * @topic: The topic. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Publishes an MQTT message. + */ +void +fb_api_publish(FbApi *api, const gchar *topic, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_api_read: + * @api: The #FbApi. + * @id: The user or thread #FbId. + * @thread: #TRUE if @id is a thread, otherwise #FALSE. + * + * Marks a message thread as read. + */ +void +fb_api_read(FbApi *api, FbId id, gboolean thread); + +/** + * fb_api_unread: + * @api: The #FbApi. + * + * Sends an unread message request. + */ +void +fb_api_unread(FbApi *api); + +/** + * fb_api_thread: + * @api: The #FbApi. + * @tid: The thread #FbId. + * + * Sends a thread request. This will obtain the general information of + * a single thread. + */ +void +fb_api_thread(FbApi *api, FbId tid); + +/** + * fb_api_thread_create: + * @api: The #FbApi. + * @uids: The #GSList of #FbId's. + * + * Sends a thread creation request. In order to create a thread, there + * must be at least two other users in @uids. + */ +void +fb_api_thread_create(FbApi *api, GSList *uids); + +/** + * fb_api_thread_invite: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @uid: The user #FbId. + * + * Sends a thread user invitation request. + */ +void +fb_api_thread_invite(FbApi *api, FbId tid, FbId uid); + +/** + * fb_api_thread_remove: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @uid: The user #FbId. + * + * Sends a thread user removal request. + */ +void +fb_api_thread_remove(FbApi *api, FbId tid, FbId uid); + +/** + * fb_api_thread_topic: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @topic: The topic. + * + * Sends a thread topic change request. + */ +void +fb_api_thread_topic(FbApi *api, FbId tid, const gchar *topic); + +/** + * fb_api_threads: + * @api: The #FbApi. + * + * Sends a threads request. This will obtain a full list of detailed + * thread information about the threads of the #FbApi user. + */ +void +fb_api_threads(FbApi *api); + +/** + * fb_api_typing: + * @api: The #FbApi. + * @uid: The user #FbId. + * @state: #TRUE if the #FbApi user is typing, otherwise #FALSE. + * + * Sends a typing state message for the user of the #FbApi. + */ +void +fb_api_typing(FbApi *api, FbId uid, gboolean state); + +/** + * fb_api_event_dup: + * @event: The #FbApiEvent or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiEvent. If @event is #NULL, a new zero filled + * #FbApiEvent is returned. The returned #FbApiEvent should be freed + * with #fb_api_event_free() when no longer needed. + * + * Returns: The new #FbApiEvent. + */ +FbApiEvent * +fb_api_event_dup(const FbApiEvent *event, gboolean deep); + +/** + * fb_api_event_reset: + * @event: The #FbApiEvent. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiEvent. + */ +void +fb_api_event_reset(FbApiEvent *event, gboolean deep); + +/** + * fb_api_event_free: + * @event: The #FbApiEvent. + * + * Frees all memory used by the #FbApiEvent. + */ +void +fb_api_event_free(FbApiEvent *event); + +/** + * fb_api_message_dup: + * @msg: The #FbApiMessage or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiMessage. If @msg is #NULL, a new zero filled + * #FbApiMessage is returned. The returned #FbApiMessage should be + * freed with #fb_api_message_free() when no longer needed. + * + * Returns: The new #FbApiMessage. + */ +FbApiMessage * +fb_api_message_dup(const FbApiMessage *msg, gboolean deep); + +/** + * fb_api_message_reset: + * @msg: The #FbApiMessage. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiMessage. + */ +void +fb_api_message_reset(FbApiMessage *msg, gboolean deep); + +/** + * fb_api_message_free: + * @msg: The #FbApiMessage. + * + * Frees all memory used by the #FbApiMessage. + */ +void +fb_api_message_free(FbApiMessage *msg); + +/** + * fb_api_presence_dup: + * @pres: The #FbApiPresence or #NULL. + * + * Duplicates an #FbApiPresence. If @pres is #NULL, a new zero filled + * #FbApiPresence is returned. The returned #FbApiPresence should be + * freed with #fb_api_presence_free() when no longer needed. + * + * Returns: The new #FbApiPresence. + */ +FbApiPresence * +fb_api_presence_dup(const FbApiPresence *pres); + +/** + * fb_api_presence_reset: + * @pres: The #FbApiPresence. + * + * Resets an #FbApiPresence. + */ +void +fb_api_presence_reset(FbApiPresence *pres); + +/** + * fb_api_presence_free: + * @pres: The #FbApiPresence. + * + * Frees all memory used by the #FbApiPresence. + */ +void +fb_api_presence_free(FbApiPresence *pres); + +/** + * fb_api_thread_dup: + * @thrd: The #FbApiThread or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiThread. If @thrd is #NULL, a new zero filled + * #FbApiThread is returned. The returned #FbApiThread should be freed + * with #fb_api_thread_free() when no longer needed. + * + * Returns: The new #FbApiThread. + */ +FbApiThread * +fb_api_thread_dup(const FbApiThread *thrd, gboolean deep); + +/** + * fb_api_thread_reset: + * @thrd: The #FbApiThread. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiThread. + */ +void +fb_api_thread_reset(FbApiThread *thrd, gboolean deep); + +/** + * fb_api_thread_free: + * @thrd: The #FbApiThread. + * + * Frees all memory used by the #FbApiThread. + */ +void +fb_api_thread_free(FbApiThread *thrd); + +/** + * fb_api_typing_dup: + * @typg: The #FbApiTyping or #NULL. + * + * Duplicates an #FbApiTyping. If @typg is #NULL, a new zero filled + * #FbApiTyping is returned. The returned #FbApiTyping should be freed + * with #fb_api_typing_free() when no longer needed. + * + * Returns: The new #FbApiTyping. + */ +FbApiTyping * +fb_api_typing_dup(const FbApiTyping *typg); + +/** + * fb_api_typing_reset: + * @typg: The #FbApiTyping. + * + * Resets an #FbApiTyping. + */ +void +fb_api_typing_reset(FbApiTyping *typg); + +/** + * fb_api_typing_free: + * @typg: The #FbApiTyping. + * + * Frees all memory used by the #FbApiTyping. + */ +void +fb_api_typing_free(FbApiTyping *typg); + +/** + * fb_api_user_dup: + * @user: The #FbApiUser or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiUser. If @user is #NULL, a new zero filled + * #FbApiUser is returned. The returned #FbApiUser should be freed with + * #fb_api_user_free() when no longer needed. + * + * Returns: The new #FbApiUser. + */ +FbApiUser * +fb_api_user_dup(const FbApiUser *user, gboolean deep); + +/** + * fb_api_user_reset: + * @user: The #FbApiUser. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiUser. + */ +void +fb_api_user_reset(FbApiUser *user, gboolean deep); + +/** + * fb_api_user_free: + * @user: The #FbApiUser. + * + * Frees all memory used by the #FbApiUser. + */ +void +fb_api_user_free(FbApiUser *user); + +#endif /* _FACEBOOK_API_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/data.c b/pidgin/libpurple/protocols/facebook/data.c new file mode 100644 index 0000000..fe32404 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/data.c @@ -0,0 +1,608 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include <string.h> + +#include "account.h" +#include "glibcompat.h" + +#include "api.h" +#include "data.h" + +struct _FbDataPrivate +{ + FbApi *api; + FbHttpConns *cons; + PurpleConnection *gc; + PurpleRoomlist *roomlist; + GQueue *msgs; + GHashTable *imgs; + GHashTable *unread; + GHashTable *evs; +}; + +struct _FbDataImagePrivate +{ + FbData *fata; + gchar *url; + FbDataImageFunc func; + gpointer data; + GDestroyNotify dunc; + + gboolean active; + const guint8 *image; + gsize size; +}; + +static const gchar *fb_props_strs[] = { + "cid", + "did", + "stoken", + "token" +}; + +G_DEFINE_TYPE(FbData, fb_data, G_TYPE_OBJECT); +G_DEFINE_TYPE(FbDataImage, fb_data_image, G_TYPE_OBJECT); + +static void +fb_data_dispose(GObject *obj) +{ + FbDataPrivate *priv = FB_DATA(obj)->priv; + GHashTableIter iter; + gpointer ptr; + + fb_http_conns_cancel_all(priv->cons); + g_hash_table_iter_init(&iter, priv->evs); + + while (g_hash_table_iter_next(&iter, NULL, &ptr)) { + g_source_remove(GPOINTER_TO_UINT(ptr)); + } + + if (G_LIKELY(priv->api != NULL)) { + g_object_unref(priv->api); + } + + fb_http_conns_free(priv->cons); + g_queue_free_full(priv->msgs, (GDestroyNotify) fb_api_message_free); + + g_hash_table_destroy(priv->imgs); + g_hash_table_destroy(priv->unread); + g_hash_table_destroy(priv->evs); +} + +static void +fb_data_class_init(FbDataClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_data_dispose; + g_type_class_add_private(klass, sizeof (FbDataPrivate)); +} + +static void +fb_data_init(FbData *fata) +{ + FbDataPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(fata, FB_TYPE_DATA, FbDataPrivate); + fata->priv = priv; + + priv->cons = fb_http_conns_new(); + priv->msgs = g_queue_new(); + + priv->imgs = g_hash_table_new_full(g_direct_hash, g_direct_equal, + g_object_unref, NULL); + priv->unread = g_hash_table_new_full(fb_id_hash, fb_id_equal, + g_free, NULL); + priv->evs = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, NULL); +} + +static void +fb_data_image_dispose(GObject *obj) +{ + FbDataImage *img = FB_DATA_IMAGE(obj); + FbDataImagePrivate *priv = img->priv; + FbData *fata = priv->fata; + + if ((priv->dunc != NULL) && (priv->data != NULL)) { + priv->dunc(priv->data); + } + + g_free(priv->url); + g_hash_table_steal(fata->priv->imgs, img); +} + +static void +fb_data_image_class_init(FbDataImageClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_data_image_dispose; + g_type_class_add_private(klass, sizeof (FbDataImagePrivate)); +} + +static void +fb_data_image_init(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(img, FB_TYPE_DATA_IMAGE, + FbDataImagePrivate); + img->priv = priv; +} + +FbData * +fb_data_new(PurpleConnection *gc) +{ + FbData *fata; + FbDataPrivate *priv; + + fata = g_object_new(FB_TYPE_DATA, NULL); + priv = fata->priv; + + priv->api = fb_api_new(gc); + priv->gc = gc; + + return fata; +} + +gboolean +fb_data_load(FbData *fata) +{ + const gchar *str; + FbDataPrivate *priv; + FbId id; + gboolean ret = TRUE; + guint i; + guint64 uint; + GValue val = G_VALUE_INIT; + PurpleAccount *acct; + + g_return_val_if_fail(FB_IS_DATA(fata), FALSE); + priv = fata->priv; + acct = purple_connection_get_account(priv->gc); + + for (i = 0; i < G_N_ELEMENTS(fb_props_strs); i++) { + str = purple_account_get_string(acct, fb_props_strs[i], NULL); + + if (str == NULL) { + ret = FALSE; + } + + g_value_init(&val, G_TYPE_STRING); + g_value_set_string(&val, str); + g_object_set_property(G_OBJECT(priv->api), fb_props_strs[i], + &val); + g_value_unset(&val); + } + + str = purple_account_get_string(acct, "mid", NULL); + + if (str != NULL) { + uint = g_ascii_strtoull(str, NULL, 10); + g_value_init(&val, G_TYPE_UINT64); + g_value_set_uint64(&val, uint); + g_object_set_property(G_OBJECT(priv->api), "mid", &val); + g_value_unset(&val); + } else { + ret = FALSE; + } + + str = purple_account_get_string(acct, "uid", NULL); + + if (str != NULL) { + id = FB_ID_FROM_STR(str); + g_value_init(&val, FB_TYPE_ID); + g_value_set_int64(&val, id); + g_object_set_property(G_OBJECT(priv->api), "uid", &val); + g_value_unset(&val); + } else { + ret = FALSE; + } + + fb_api_rehash(priv->api); + return ret; +} + +void +fb_data_save(FbData *fata) +{ + const gchar *str; + FbDataPrivate *priv; + gchar *dup; + guint i; + guint64 uint; + GValue val = G_VALUE_INIT; + PurpleAccount *acct; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + acct = purple_connection_get_account(priv->gc); + + for (i = 0; i < G_N_ELEMENTS(fb_props_strs); i++) { + g_value_init(&val, G_TYPE_STRING); + g_object_get_property(G_OBJECT(priv->api), fb_props_strs[i], + &val); + str = g_value_get_string(&val); + + if (purple_strequal(fb_props_strs[i], "token") && !purple_account_get_remember_password(acct)) { + str = ""; + } + purple_account_set_string(acct, fb_props_strs[i], str); + g_value_unset(&val); + } + + g_value_init(&val, G_TYPE_UINT64); + g_object_get_property(G_OBJECT(priv->api), "mid", &val); + uint = g_value_get_uint64(&val); + g_value_unset(&val); + + dup = g_strdup_printf("%" G_GINT64_FORMAT, uint); + purple_account_set_string(acct, "mid", dup); + g_free(dup); + + g_value_init(&val, G_TYPE_INT64); + g_object_get_property(G_OBJECT(priv->api), "uid", &val); + uint = g_value_get_int64(&val); + g_value_unset(&val); + + dup = g_strdup_printf("%" FB_ID_FORMAT, uint); + purple_account_set_string(acct, "uid", dup); + g_free(dup); +} + +void +fb_data_add_timeout(FbData *fata, const gchar *name, guint interval, + GSourceFunc func, gpointer data) +{ + FbDataPrivate *priv; + gchar *key; + guint id; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + fb_data_clear_timeout(fata, name, TRUE); + + key = g_strdup(name); + id = g_timeout_add(interval, func, data); + g_hash_table_replace(priv->evs, key, GUINT_TO_POINTER(id)); +} + +void +fb_data_clear_timeout(FbData *fata, const gchar *name, gboolean remove) +{ + FbDataPrivate *priv; + gpointer ptr; + guint id; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + ptr = g_hash_table_lookup(priv->evs, name); + id = GPOINTER_TO_UINT(ptr); + + if ((id > 0) && remove) { + g_source_remove(id); + } + + g_hash_table_remove(priv->evs, name); +} + +FbApi * +fb_data_get_api(FbData *fata) +{ + FbDataPrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + + return priv->api; +} + +PurpleConnection * +fb_data_get_connection(FbData *fata) +{ + FbDataPrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + + return priv->gc; +} + +PurpleRoomlist * +fb_data_get_roomlist(FbData *fata) +{ + FbDataPrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + + return priv->roomlist; +} + +gboolean +fb_data_get_unread(FbData *fata, FbId id) +{ + FbDataPrivate *priv; + gpointer *ptr; + + g_return_val_if_fail(FB_IS_DATA(fata), FALSE); + g_return_val_if_fail(id != 0, FALSE); + priv = fata->priv; + + ptr = g_hash_table_lookup(priv->unread, &id); + return GPOINTER_TO_INT(ptr); +} + +void +fb_data_set_roomlist(FbData *fata, PurpleRoomlist *list) +{ + FbDataPrivate *priv; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + priv->roomlist = list; +} + +void +fb_data_set_unread(FbData *fata, FbId id, gboolean unread) +{ + FbDataPrivate *priv; + gpointer key; + + g_return_if_fail(FB_IS_DATA(fata)); + g_return_if_fail(id != 0); + priv = fata->priv; + + if (!unread) { + g_hash_table_remove(priv->unread, &id); + return; + } + + key = g_memdup(&id, sizeof id); + g_hash_table_replace(priv->unread, key, GINT_TO_POINTER(unread)); +} + +void +fb_data_add_message(FbData *fata, FbApiMessage *msg) +{ + FbDataPrivate *priv; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + g_queue_push_tail(priv->msgs, msg); +} + +void +fb_data_remove_message(FbData *fata, FbApiMessage *msg) +{ + FbDataPrivate *priv; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + g_queue_remove(priv->msgs, msg); +} + +GSList * +fb_data_take_messages(FbData *fata, FbId uid) +{ + FbApiMessage *msg; + FbDataPrivate *priv; + GList *l; + GList *prev; + GSList *msgs = NULL; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + l = priv->msgs->tail; + + while (l != NULL) { + msg = l->data; + prev = l->prev; + + if (msg->uid == uid) { + msgs = g_slist_prepend(msgs, msg); + g_queue_delete_link(priv->msgs, l); + } + + l = prev; + } + + return msgs; +} + +FbDataImage * +fb_data_image_add(FbData *fata, const gchar *url, FbDataImageFunc func, + gpointer data, GDestroyNotify dunc) +{ + FbDataImage *img; + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + g_return_val_if_fail(url != NULL, NULL); + g_return_val_if_fail(func != NULL, NULL); + + img = g_object_new(FB_TYPE_DATA_IMAGE, NULL); + priv = img->priv; + + priv->fata = fata; + priv->url = g_strdup(url); + priv->func = func; + priv->data = data; + priv->dunc = dunc; + + g_hash_table_insert(fata->priv->imgs, img, img); + return img; +} + +gboolean +fb_data_image_get_active(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), FALSE); + priv = img->priv; + + return priv->active; +} + +gpointer +fb_data_image_get_data(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + return priv->data; +} + +FbData * +fb_data_image_get_fata(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + return priv->fata; +} + +const guint8 * +fb_data_image_get_image(FbDataImage *img, gsize *size) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + if (size != NULL) { + *size = priv->size; + } + + return priv->image; +} + +guint8 * +fb_data_image_dup_image(FbDataImage *img, gsize *size) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + if (size != NULL) { + *size = priv->size; + } + + if (priv->size < 1) { + return NULL; + } + + return g_memdup(priv->image, priv->size); +} + +const gchar * +fb_data_image_get_url(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + return priv->url; +} + +static void +fb_data_image_cb(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbDataImage *img = data; + FbDataImagePrivate *priv = img->priv; + FbDataPrivate *driv = priv->fata->priv; + GError *err = NULL; + + if (fb_http_conns_is_canceled(driv->cons)) { + return; + } + + fb_http_conns_remove(driv->cons, con); + fb_http_error_chk(res, &err); + + priv->image = (guint8 *) purple_http_response_get_data(res, &priv->size); + priv->func(img, err); + + if (G_LIKELY(err == NULL)) { + fb_data_image_queue(priv->fata); + } else { + g_error_free(err); + } + + g_object_unref(img); +} + +void +fb_data_image_queue(FbData *fata) +{ + const gchar *url; + FbDataImage *img; + FbDataPrivate *priv; + GHashTableIter iter; + guint active = 0; + PurpleHttpConnection *con; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + g_hash_table_iter_init(&iter, priv->imgs); + + while (g_hash_table_iter_next(&iter, (gpointer *) &img, NULL)) { + if (fb_data_image_get_active(img)) { + active++; + } + } + + if (active >= FB_DATA_ICON_MAX) { + return; + } + + g_hash_table_iter_init(&iter, priv->imgs); + + while (g_hash_table_iter_next(&iter, (gpointer *) &img, NULL)) { + if (fb_data_image_get_active(img)) { + continue; + } + + img->priv->active = TRUE; + url = fb_data_image_get_url(img); + con = purple_http_get(priv->gc, fb_data_image_cb, img, url); + fb_http_conns_add(priv->cons, con); + + if (++active >= FB_DATA_ICON_MAX) { + break; + } + } +} diff --git a/pidgin/libpurple/protocols/facebook/data.h b/pidgin/libpurple/protocols/facebook/data.h new file mode 100644 index 0000000..dbcac47 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/data.h @@ -0,0 +1,398 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_DATA_H_ +#define _FACEBOOK_DATA_H_ + +/** + * SECTION:data + * @section_id: facebook-data + * @short_description: <filename>data.h</filename> + * @title: Connection Data + * + * The Connection Data. + */ + +#include <glib.h> + +#include "connection.h" +#include "roomlist.h" + +#include "api.h" +#include "http.h" +#include "id.h" + +#define FB_TYPE_DATA (fb_data_get_type()) +#define FB_DATA(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_DATA, FbData)) +#define FB_DATA_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_DATA, FbDataClass)) +#define FB_IS_DATA(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_DATA)) +#define FB_IS_DATA_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_DATA)) +#define FB_DATA_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_DATA, FbDataClass)) + +#define FB_TYPE_DATA_IMAGE (fb_data_image_get_type()) +#define FB_DATA_IMAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_DATA_IMAGE, FbDataImage)) +#define FB_DATA_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_DATA_IMAGE, FbDataImageClass)) +#define FB_IS_DATA_IMAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_DATA_IMAGE)) +#define FB_IS_DATA_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_DATA_IMAGE)) +#define FB_DATA_IMAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_DATA_IMAGE, FbDataImageClass)) + +/** + * FB_DATA_ICON_MAX: + * + * The maximum of number of concurrent icon fetches. + */ +#define FB_DATA_ICON_MAX 4 + +typedef struct _FbData FbData; +typedef struct _FbDataClass FbDataClass; +typedef struct _FbDataPrivate FbDataPrivate; +typedef struct _FbDataImage FbDataImage; +typedef struct _FbDataImageClass FbDataImageClass; +typedef struct _FbDataImagePrivate FbDataImagePrivate; + +/** + * FbDataImageFunc: + * @img: The #FbDataImage. + * @error: The #GError or #NULL. + * + * The callback for a fetched #FbDataImage. + */ +typedef void (*FbDataImageFunc) (FbDataImage *img, GError *error); + +/** + * FbData: + * + * Represents the connection data used by #FacebookProtocol. + */ +struct _FbData +{ + /*< private >*/ + GObject parent; + FbDataPrivate *priv; +}; + +/** + * FbDataClass: + * + * The base class for all #FbData's. + */ +struct _FbDataClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * FbDataImage: + * + * Represents the data used for fetching images. + */ +struct _FbDataImage +{ + /*< private >*/ + GObject parent; + FbDataImagePrivate *priv; +}; + +/** + * FbDataImageClass: + * + * The base class for all #FbDataImage's. + */ +struct _FbDataImageClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_data_get_type: + * + * Returns: The #GType for an #FbData. + */ +GType +fb_data_get_type(void); + +/** + * fb_data_image_get_type: + * + * Returns: The #GType for an #FbDataImage. + */ +GType +fb_data_image_get_type(void); + +/** + * fb_data_new: + * @gc: The #PurpleConnection. + * + * Creates a new #FbData. The returned #FbData should be freed with + * #g_object_unref() when no longer needed. + * + * Returns: The new #FbData. + */ +FbData * +fb_data_new(PurpleConnection *gc); + +/** + * fb_data_load: + * @fata: The #FbData. + * + * Loads the internal data from the underlying #PurpleAccount. + * + * Return: #TRUE if all of the data was loaded, otherwise #FALSE. + */ +gboolean +fb_data_load(FbData *fata); + +/** + * fb_data_save: + * @fata: The #FbData. + * + * Saves the internal data to the underlying #PurpleAccount. + */ +void +fb_data_save(FbData *fata); + +/** + * fb_data_add_timeout: + * @fata: The #FbData. + * @name: The name of the timeout. + * @interval: The time, in milliseconds, between calls to @func. + * @func: The #GSourceFunc. + * @data: The data passed to @func. + * + * Adds a new callback timer. The callback is called repeatedly on the + * basis of @interval, until @func returns #FALSE. The timeout should + * be cleared with #fb_data_clear_timeout() when no longer needed. + */ +void +fb_data_add_timeout(FbData *fata, const gchar *name, guint interval, + GSourceFunc func, gpointer data); + +/** + * fb_data_clear_timeout: + * @fata: The #FbData. + * @name: The name of the timeout. + * @remove: #TRUE to remove from the event loop, otherwise #FALSE. + * + * Clears and removes a callback timer. The only time @remove should be + * #FALSE, is when being called from a #GSourceFunc, which is returning + * #FALSE. + */ +void +fb_data_clear_timeout(FbData *fata, const gchar *name, gboolean remove); + +/** + * fb_data_get_api: + * @fata: The #FbData. + * + * Gets the #FbApi from the #FbData. + * + * Return: The #FbApi. + */ +FbApi * +fb_data_get_api(FbData *fata); + +/** + * fb_data_get_connection: + * @fata: The #FbData. + * + * Gets the #PurpleConnection from the #FbData. + * + * Return: The #PurpleConnection. + */ +PurpleConnection * +fb_data_get_connection(FbData *fata); + +/** + * fb_data_get_roomlist: + * @fata: The #FbData. + * + * Gets the #PurpleRoomlist from the #FbData. + * + * Return: The #PurpleRoomlist. + */ +PurpleRoomlist * +fb_data_get_roomlist(FbData *fata); + +/** + * fb_data_get_unread: + * @fata: The #FbData. + * @id: The #FbId. + * + * Gets the unread state of an #FbId. + * + * Return: #TRUE if the #FbId is unread, otherwise #FALSE. + */ +gboolean +fb_data_get_unread(FbData *fata, FbId id); + +/** + * fb_data_set_roomlist: + * @fata: The #FbData. + * @list: The #PurpleRoomlist. + * + * Sets the #PurpleRoomlist to the #FbData. + */ +void +fb_data_set_roomlist(FbData *fata, PurpleRoomlist *list); + +/** + * fb_data_set_unread: + * @fata: The #FbData. + * @id: The #FbId. + * @unread: #TRUE if the #FbId is unread, otherwise #FALSE. + * + * Sets the unread state of an #FbId to the #FbData. + */ +void +fb_data_set_unread(FbData *fata, FbId id, gboolean unread); + +/** + * fb_data_add_message: + * @fata: The #FbData. + * @msg: The #FbApiMessage. + * + * Adds an #FbApiMessage to the #FbData. + */ +void +fb_data_add_message(FbData *fata, FbApiMessage *msg); + +/** + * fb_data_remove_message: + * @fata: The #FbData. + * @msg: The #FbApiMessage. + * + * Removes an #FbApiMessage from the #FbData. + */ +void +fb_data_remove_message(FbData *fata, FbApiMessage *msg); + +/** + * fb_data_take_messages: + * @fata: The #FbData. + * @uid: The user #FbId. + * + * Gets a #GSList of messages by the user #FbId from the #FbData. The + * #FbApiMessage's are removed from the #FbData. The returned #GSList + * and its #FbApiMessage's should be freed with #fb_api_message_free() + * and #g_slist_free_full() when no longer needed. + */ +GSList * +fb_data_take_messages(FbData *fata, FbId uid); + +/** + * fb_data_image_add: + * @fata: The #FbData. + * @url: The image URL. + * @func: The #FbDataImageFunc. + * @data: The user-defined data. + * @dunc: The #GDestroyNotify for @data or #NULL. + * + * Adds a new #FbDataImage to the #FbData. This is used to fetch images + * from HTTP sources. After calling this, #fb_data_image_queue() should + * be called to queue the fetching process. + * + * Return: The #FbDataImage. + */ +FbDataImage * +fb_data_image_add(FbData *fata, const gchar *url, FbDataImageFunc func, + gpointer data, GDestroyNotify dunc); + +/** + * fb_data_image_get_active: + * @img: The #FbDataImage. + * + * Gets the active fetching state from the #FbDataImage. + * + * Returns: #TRUE if the image is being fetched, otherwise #FALSE. + */ +gboolean +fb_data_image_get_active(FbDataImage *img); + +/** + * fb_data_image_get_data: + * @img: The #FbDataImage. + * + * Gets the user-defined data from the #FbDataImage. + * + * Returns: The user-defined data. + */ +gpointer +fb_data_image_get_data(FbDataImage *img); + +/** + * fb_data_image_get_fata: + * @img: The #FbDataImage. + * + * Gets the #FbData from the #FbDataImage. + * + * Returns: The #FbData. + */ +FbData * +fb_data_image_get_fata(FbDataImage *img); + +/** + * fb_data_image_get_image: + * @img: The #FbDataImage. + * @size: The return location for the image size or #NULL. + * + * Gets the image data from the #FbDataImage. + * + * Returns: The image data. + */ +const guint8 * +fb_data_image_get_image(FbDataImage *img, gsize *size); + +/** + * fb_data_image_dup_image: + * @img: The #FbDataImage. + * @size: The return location for the image size or #NULL. + * + * Gets the duplicated image data from the #FbDataImage. The returned + * data should be freed with #g_free() when no longer needed. + * + * Returns: The duplicated image data. + */ +guint8 * +fb_data_image_dup_image(FbDataImage *img, gsize *size); + +/** + * fb_data_image_get_url: + * @img: The #FbDataImage. + * + * Gets the image URL from the #FbDataImage. + * + * Returns: The image URL. + */ +const gchar * +fb_data_image_get_url(FbDataImage *img); + +/** + * fb_data_image_queue: + * @fata: The #FbData. + * + * Queues the next #FbDataImage fetches. + */ +void +fb_data_image_queue(FbData *fata); + +#endif /* _FACEBOOK_DATA_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/facebook.c b/pidgin/libpurple/protocols/facebook/facebook.c new file mode 100644 index 0000000..9554adc --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/facebook.c @@ -0,0 +1,1713 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include "account.h" +#include "accountopt.h" +#include "blistnode.h" +#include "buddy.h" +#include "buddyicon.h" +#include "buddylist.h" +#include "cmds.h" +#include "connection.h" +#include "conversation.h" +#include "conversations.h" +#include "conversationtypes.h" +#include "glibcompat.h" +#include "image.h" +#include "image-store.h" +#include "message.h" +#include "notify.h" +#include "plugins.h" +#include "presence.h" +#include "protocol.h" +#include "protocols.h" +#include "request.h" +#include "roomlist.h" +#include "server.h" +#include "signals.h" +#include "sslconn.h" +#include "status.h" +#include "util.h" +#include "version.h" + +#include "api.h" +#include "data.h" +#include "facebook.h" +#include "http.h" +#include "util.h" + +static GSList *fb_cmds = NULL; +static PurpleProtocol *fb_protocol = NULL; + +static void +fb_cb_api_messages(FbApi *api, GSList *msgs, gpointer data); + +static PurpleGroup * +fb_get_group(gboolean friend) +{ + PurpleBlistNode *n; + PurpleBlistNode *node; + PurpleGroup *grp; + const gchar *title; + + if (friend) { + title = _("Facebook Friends"); + } else { + title = _("Facebook Non-Friends"); + } + + grp = purple_blist_find_group(title); + + if (G_UNLIKELY(grp == NULL)) { + grp = purple_group_new(title); + node = NULL; + + for (n = purple_blist_get_root(); n != NULL; n = n->next) { + node = n; + } + + /* Append to the end of the buddy list */ + purple_blist_add_group(grp, node); + + if (!friend) { + node = PURPLE_BLIST_NODE(grp); + purple_blist_node_set_bool(node, "collapsed", TRUE); + } + } + + return grp; +} + +static void +fb_buddy_add_nonfriend(PurpleAccount *acct, FbApiUser *user) +{ + gchar uid[FB_ID_STRMAX]; + PurpleBuddy *bdy; + PurpleGroup *grp; + + FB_ID_TO_STR(user->uid, uid); + bdy = purple_buddy_new(acct, uid, user->name); + grp = fb_get_group(FALSE); + + purple_buddy_set_server_alias(bdy, user->name); + purple_blist_add_buddy(bdy, NULL, grp, NULL); +} + +static void +fb_cb_api_auth(FbApi *api, gpointer data) +{ + FbData *fata = data; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + + purple_connection_update_progress(gc, _("Fetching contacts"), 2, 4); + fb_data_save(fata); + fb_api_contacts(api); +} + +static void +fb_cb_api_connect(FbApi *api, gpointer data) +{ + FbData *fata = data; + PurpleAccount *acct; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + + fb_data_save(fata); + purple_connection_set_state(gc, PURPLE_CONNECTION_CONNECTED); + + if (purple_account_get_bool(acct, "show-unread", TRUE)) { + fb_api_unread(api); + } +} + +static void +fb_cb_api_contact(FbApi *api, FbApiUser *user, gpointer data) +{ + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + GSList *msgs; + PurpleAccount *acct; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + FB_ID_TO_STR(user->uid, uid); + + if (purple_blist_find_buddy(acct, uid) == NULL) { + fb_buddy_add_nonfriend(acct, user); + } + + msgs = fb_data_take_messages(fata, user->uid); + + if (msgs != NULL) { + fb_cb_api_messages(api, msgs, fata); + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + } +} + +static gboolean +fb_cb_sync_contacts(gpointer data) +{ + FbApi *api; + FbData *fata = data; + + api = fb_data_get_api(fata); + fb_data_clear_timeout(fata, "sync-contacts", FALSE); + fb_api_contacts(api); + return FALSE; +} + +static void +fb_cb_icon(FbDataImage *img, GError *error) +{ + const gchar *csum; + const gchar *name; + const gchar *str; + FbHttpParams *params; + gsize size; + guint8 *image; + PurpleAccount *acct; + PurpleBuddy *bdy; + + bdy = fb_data_image_get_data(img); + acct = purple_buddy_get_account(bdy); + name = purple_buddy_get_name(bdy); + + if (G_UNLIKELY(error != NULL)) { + fb_util_debug_warning("Failed to retrieve icon for %s: %s", + name, error->message); + return; + } + + str = fb_data_image_get_url(img); + params = fb_http_params_new_parse(str, TRUE); + csum = fb_http_params_get_str(params, "oh", NULL); + + image = fb_data_image_dup_image(img, &size); + purple_buddy_icons_set_for_user(acct, name, image, size, csum); + fb_http_params_free(params); +} + +static void +fb_sync_contacts_add_timeout(FbData *fata) +{ + gint sync; + PurpleConnection *gc; + PurpleAccount *acct; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + + sync = purple_account_get_int(acct, "sync-interval", 5); + + if (sync < 1) { + purple_account_set_int(acct, "sync-interval", 1); + sync = 1; + } + + sync *= 60 * 1000; + fb_data_add_timeout(fata, "sync-contacts", sync, fb_cb_sync_contacts, + fata); +} + +static void +fb_cb_api_contacts(FbApi *api, GSList *users, gboolean complete, gpointer data) +{ + const gchar *alias; + const gchar *csum; + FbApiUser *user; + FbData *fata = data; + FbId muid; + gchar uid[FB_ID_STRMAX]; + GSList *l; + GValue val = G_VALUE_INIT; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleConnectionState state; + PurpleGroup *grp; + PurpleGroup *grpn; + PurpleStatus *status; + PurpleStatusPrimitive pstat; + PurpleStatusType *type; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + grp = fb_get_group(TRUE); + grpn = fb_get_group(FALSE); + alias = purple_account_get_private_alias(acct); + state = purple_connection_get_state(gc); + + g_value_init(&val, FB_TYPE_ID); + g_object_get_property(G_OBJECT(api), "uid", &val); + muid = g_value_get_int64(&val); + g_value_unset(&val); + + for (l = users; l != NULL; l = l->next) { + user = l->data; + FB_ID_TO_STR(user->uid, uid); + + if (G_UNLIKELY(user->uid == muid)) { + if (G_UNLIKELY(alias != NULL)) { + continue; + } + + purple_account_set_private_alias(acct, user->name); + continue; + } + + bdy = purple_blist_find_buddy(acct, uid); + + if ((bdy != NULL) && (purple_buddy_get_group(bdy) == grpn)) { + purple_blist_remove_buddy(bdy); + bdy = NULL; + } + + if (bdy == NULL) { + bdy = purple_buddy_new(acct, uid, NULL); + purple_blist_add_buddy(bdy, NULL, grp, NULL); + } + + purple_buddy_set_server_alias(bdy, user->name); + csum = purple_buddy_icons_get_checksum_for_user(bdy); + + if (!purple_strequal(csum, user->csum)) { + fb_data_image_add(fata, user->icon, fb_cb_icon, + bdy, NULL); + } + } + + fb_data_image_queue(fata); + + if (!complete) { + return; + } + + if (state != PURPLE_CONNECTION_CONNECTED) { + status = purple_account_get_active_status(acct); + type = purple_status_get_status_type(status); + pstat = purple_status_type_get_primitive(type); + + purple_connection_update_progress(gc, _("Connecting"), 3, 4); + fb_api_connect(api, pstat == PURPLE_STATUS_INVISIBLE); + } + + fb_sync_contacts_add_timeout(fata); +} + +static void +fb_cb_api_contacts_delta(FbApi *api, GSList *added, GSList *removed, gpointer data) +{ + FbApiUser *user; + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + GSList *l; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleGroup *grp; + PurpleGroup *grpn; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + grp = fb_get_group(TRUE); + grpn = fb_get_group(FALSE); + + for (l = added; l != NULL; l = l->next) { + user = l->data; + FB_ID_TO_STR(user->uid, uid); + + bdy = purple_blist_find_buddy(acct, uid); + + if ((bdy != NULL) && (purple_buddy_get_group(bdy) == grpn)) { + purple_blist_remove_buddy(bdy); + } + + bdy = purple_buddy_new(acct, uid, NULL); + purple_blist_add_buddy(bdy, NULL, grp, NULL); + + purple_buddy_set_server_alias(bdy, user->name); + } + + for (l = removed; l != NULL; l = l->next) { + bdy = purple_blist_find_buddy(acct, l->data); + + if (bdy != NULL) { + purple_blist_remove_buddy(bdy); + } + } + + fb_sync_contacts_add_timeout(fata); +} + +static void +fb_cb_api_error(FbApi *api, GError *error, gpointer data) +{ + FbData *fata = data; + PurpleConnection *gc; + PurpleConnectionError errc; + + gc = fb_data_get_connection(fata); + + if (error->domain == G_IO_ERROR) { + purple_connection_g_error(gc, error); + return; + } + + if (g_error_matches(error, FB_API_ERROR, FB_API_ERROR_QUEUE)) { + /* Save the reset data */ + fb_data_save(fata); + } + + if ((error->domain == FB_HTTP_ERROR) && + (error->code >= 400) && + (error->code <= 500)) + { + errc = PURPLE_CONNECTION_ERROR_OTHER_ERROR; + } else if (g_error_matches(error, FB_API_ERROR, FB_API_ERROR_AUTH)) { + errc = PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED; + } else { + errc = PURPLE_CONNECTION_ERROR_NETWORK_ERROR; + } + + + if (!g_error_matches(error, FB_API_ERROR, FB_API_ERROR_NONFATAL)) { + purple_connection_error(gc, errc, error->message); + } +} + +static void +fb_cb_api_events(FbApi *api, GSList *events, gpointer data) +{ + FbData *fata = data; + FbApiEvent *event; + gchar uid[FB_ID_STRMAX]; + gchar tid[FB_ID_STRMAX]; + GHashTable *fetch; + GHashTableIter iter; + GSList *l; + PurpleAccount *acct; + PurpleChatConversation *chat; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + fetch = g_hash_table_new(fb_id_hash, fb_id_equal); + + for (l = events; l != NULL; l = l->next) { + event = l->data; + + FB_ID_TO_STR(event->tid, tid); + chat = purple_conversations_find_chat_with_account(tid, acct); + + if (chat == NULL) { + continue; + } + + FB_ID_TO_STR(event->uid, uid); + + switch (event->type) { + case FB_API_EVENT_TYPE_THREAD_TOPIC: + purple_chat_conversation_set_topic(chat, uid, + event->text); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_ADDED: + if (purple_blist_find_buddy(acct, uid) == NULL) { + if (event->text) { + FbApiUser *user = fb_api_user_dup(NULL, FALSE); + user->uid = event->uid; + user->name = g_strdup(event->text); + + fb_buddy_add_nonfriend(acct, user); + + fb_api_user_free(user); + } else { + g_hash_table_insert(fetch, &event->tid, event); + break; + } + } + + purple_chat_conversation_add_user(chat, uid, NULL, 0, + TRUE); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_REMOVED: + purple_chat_conversation_remove_user(chat, uid, event->text); + break; + } + } + + g_hash_table_iter_init(&iter, fetch); + + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &event)) { + fb_api_thread(api, event->tid); + } + + g_hash_table_destroy(fetch); +} + +static void +fb_cb_image(FbDataImage *img, GError *error) +{ + const gchar *url; + FbApi *api; + FbApiMessage *msg; + FbData *fata; + gsize size; + GSList *msgs = NULL; + guint id; + guint8 *image; + PurpleImage *pimg; + + fata = fb_data_image_get_fata(img); + msg = fb_data_image_get_data(img); + + if (G_UNLIKELY(error != NULL)) { + url = fb_data_image_get_url(img); + fb_util_debug_warning("Failed to retrieve image %s: %s", + url, error->message); + return; + } + + api = fb_data_get_api(fata); + image = fb_data_image_dup_image(img, &size); + pimg = purple_image_new_from_data(image, size); + id = purple_image_store_add_weak(pimg); + + g_free(msg->text); + msg->text = g_strdup_printf("<img src=\"" + PURPLE_IMAGE_STORE_PROTOCOL + "%u\">", id); + msg->flags |= FB_API_MESSAGE_FLAG_DONE; + + msgs = g_slist_prepend(msgs, msg); + fb_cb_api_messages(api, msgs, fata); + g_slist_free(msgs); +} + +static void +fb_cb_api_messages(FbApi *api, GSList *msgs, gpointer data) +{ + const gchar *text; + FbApiMessage *msg; + FbData *fata = data; + gboolean isself; + gboolean mark; + gboolean open; + gboolean self; + gchar *html; + gchar tid[FB_ID_STRMAX]; + gchar uid[FB_ID_STRMAX]; + gint id; + gint64 tstamp; + GSList *l; + PurpleAccount *acct; + PurpleChatConversation *chat; + PurpleConnection *gc; + PurpleMessageFlags flags; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + mark = purple_account_get_bool(acct, "mark-read", TRUE); + open = purple_account_get_bool(acct, "group-chat-open", TRUE); + self = purple_account_get_bool(acct, "show-self", TRUE); + + for (l = msgs; l != NULL; l = l->next) { + msg = l->data; + FB_ID_TO_STR(msg->uid, uid); + + if (purple_blist_find_buddy(acct, uid) == NULL) { + msg = fb_api_message_dup(msg, TRUE); + fb_data_add_message(fata, msg); + fb_api_contact(api, msg->uid); + continue; + } + + isself = (msg->flags & FB_API_MESSAGE_FLAG_SELF) != 0; + + if (isself && !self) { + continue; + } + + flags = isself ? PURPLE_MESSAGE_SEND : PURPLE_MESSAGE_RECV; + tstamp = msg->tstamp / 1000; + + if (msg->flags & FB_API_MESSAGE_FLAG_IMAGE) { + if (!(msg->flags & FB_API_MESSAGE_FLAG_DONE)) { + msg = fb_api_message_dup(msg, TRUE); + fb_data_image_add(fata, msg->text, fb_cb_image, + msg, (GDestroyNotify) + fb_api_message_free); + fb_data_image_queue(fata); + continue; + } + + flags |= PURPLE_MESSAGE_IMAGES; + text = msg->text; + html = NULL; + } else { + html = purple_markup_escape_text(msg->text, -1); + text = html; + } + + if (msg->tid == 0) { + if (mark && !isself) { + fb_data_set_unread(fata, msg->uid, TRUE); + } + + fb_util_serv_got_im(gc, uid, text, flags, tstamp); + g_free(html); + continue; + } + + FB_ID_TO_STR(msg->tid, tid); + chat = purple_conversations_find_chat_with_account(tid, acct); + + if (chat == NULL) { + if (!open) { + g_free(html); + continue; + } + + id = fb_id_hash(&msg->tid); + purple_serv_got_joined_chat(gc, id, tid); + fb_api_thread(api, msg->tid); + } else { + id = purple_chat_conversation_get_id(chat); + } + + if (mark && !isself) { + fb_data_set_unread(fata, msg->tid, TRUE); + } + + fb_util_serv_got_chat_in(gc, id, uid, text, flags, tstamp); + g_free(html); + } +} + +static void +fb_cb_api_presences(FbApi *api, GSList *press, gpointer data) +{ + const gchar *statid; + FbApiPresence *pres; + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + GSList *l; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleStatusPrimitive pstat; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + + for (l = press; l != NULL; l = l->next) { + pres = l->data; + + if (pres->active) { + pstat = PURPLE_STATUS_AVAILABLE; + } else { + pstat = PURPLE_STATUS_OFFLINE; + } + + FB_ID_TO_STR(pres->uid, uid); + statid = purple_primitive_get_id_from_type(pstat); + purple_protocol_got_user_status(acct, uid, statid, NULL); + } +} + +static void +fb_cb_api_thread(FbApi *api, FbApiThread *thrd, gpointer data) +{ + const gchar *name; + FbApiUser *user; + FbData *fata = data; + gboolean active; + gchar tid[FB_ID_STRMAX]; + gchar uid[FB_ID_STRMAX]; + gint id; + GSList *l; + PurpleAccount *acct; + PurpleChatConversation *chat; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + id = fb_id_hash(&thrd->tid); + FB_ID_TO_STR(thrd->tid, tid); + + chat = purple_conversations_find_chat_with_account(tid, acct); + + if ((chat == NULL) || purple_chat_conversation_has_left(chat)) { + chat = purple_serv_got_joined_chat(gc, id, tid); + active = FALSE; + } else { + /* If there are no users in the group chat, including + * the local user, then the group chat has yet to be + * setup by this function. As a result, any group chat + * without users is inactive. + */ + active = purple_chat_conversation_get_users_count(chat) > 0; + } + + if (!active) { + name = purple_account_get_username(acct); + purple_chat_conversation_add_user(chat, name, NULL, 0, FALSE); + } + + purple_chat_conversation_set_topic(chat, NULL, thrd->topic); + + for (l = thrd->users; l != NULL; l = l->next) { + user = l->data; + FB_ID_TO_STR(user->uid, uid); + + if (purple_chat_conversation_has_user(chat, uid)) { + continue; + } + + if (purple_blist_find_buddy(acct, uid) == NULL) { + fb_buddy_add_nonfriend(acct, user); + } + + purple_chat_conversation_add_user(chat, uid, NULL, 0, active); + } +} + +static void +fb_cb_api_thread_create(FbApi *api, FbId tid, gpointer data) +{ + FbData *fata = data; + gchar sid[FB_ID_STRMAX]; + GHashTable *table; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + FB_ID_TO_STR(tid, sid); + + table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); + g_hash_table_insert(table, "name", g_strdup(sid)); + purple_serv_join_chat(gc, table); + g_hash_table_destroy(table); +} + +static void +fb_cb_api_thread_kicked(FbApi *api, FbApiThread *thrd, gpointer data) +{ + FbData *fata = data; + gchar tid[FB_ID_STRMAX]; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleChatConversation *chat; + + FB_ID_TO_STR(thrd->tid, tid); + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + chat = purple_conversations_find_chat_with_account(tid, acct); + + if (chat == NULL) { + PurpleRequestCommonParameters *cpar; + + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Join a Chat"), + _("Failed to Join Chat"), + _("You have been removed from this chat"), + cpar); + return; + } + + purple_conversation_write_system_message(PURPLE_CONVERSATION(chat), + _("You have been removed from this chat"), 0); + + purple_serv_got_chat_left(gc, purple_chat_conversation_get_id(chat)); +} + +static void +fb_cb_api_threads(FbApi *api, GSList *thrds, gpointer data) +{ + const gchar *alias; + FbApiUser *user; + FbData *fata = data; + gchar tid[FB_ID_STRMAX]; + gchar uid[FB_ID_STRMAX]; + GSList *l; + GSList *m; + GString *gstr; + FbApiThread *thrd; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleRoomlist *list; + PurpleRoomlistRoom *room; + + list = fb_data_get_roomlist(fata); + + if (G_UNLIKELY(list == NULL)) { + return; + } + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + gstr = g_string_new(NULL); + + for (l = thrds; l != NULL; l = l->next) { + thrd = l->data; + FB_ID_TO_STR(thrd->tid, tid); + g_string_truncate(gstr, 0); + + for (m = thrd->users; m != NULL; m = m->next) { + user = m->data; + FB_ID_TO_STR(user->uid, uid); + bdy = purple_blist_find_buddy(acct, uid); + + if (bdy != NULL) { + alias = purple_buddy_get_alias(bdy); + } else { + alias = user->name; + } + + if (gstr->len > 0) { + g_string_append(gstr, ", "); + } + + g_string_append(gstr, alias); + } + + room = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_ROOM, + tid, NULL); + purple_roomlist_room_add_field(list, room, thrd->topic); + purple_roomlist_room_add_field(list, room, gstr->str); + purple_roomlist_room_add(list, room); + } + + purple_roomlist_set_in_progress(list, FALSE); + fb_data_set_roomlist(fata, NULL); + g_string_free(gstr, TRUE); +} + +static void +fb_cb_api_typing(FbApi *api, FbApiTyping *typg, gpointer data) +{ + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + FB_ID_TO_STR(typg->uid, uid); + + if (typg->state) { + purple_serv_got_typing(gc, uid, 0, PURPLE_IM_TYPING); + } else { + purple_serv_got_typing_stopped(gc, uid); + } +} + +static void +fb_mark_read(FbData *fata, FbId id, gboolean thread) +{ + FbApi *api; + PurpleAccount *acct; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + api = fb_data_get_api(fata); + + if (!fb_data_get_unread(fata, id) || + (purple_account_get_bool(acct, "mark-read-available", FALSE) && + fb_api_is_invisible(api))) + { + return; + } + + fb_data_set_unread(fata, id, FALSE); + fb_api_read(api, id, thread); +} + +static gboolean +fb_cb_conv_read(gpointer data) +{ + const gchar *name; + FbData *fata; + FbId id; + gchar *tname; + PurpleConnection *gc; + PurpleConversation *conv = data; + + gc = purple_conversation_get_connection(conv); + fata = purple_connection_get_protocol_data(gc); + name = purple_conversation_get_name(conv); + id = FB_ID_FROM_STR(name); + + tname = g_strconcat("conv-read-", name, NULL); + fb_data_clear_timeout(fata, tname, FALSE); + g_free(tname); + + if (purple_conversation_has_focus(conv)) { + fb_mark_read(fata, id, PURPLE_IS_CHAT_CONVERSATION(conv)); + } + return FALSE; +} + +static void +fb_cb_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type, + gpointer data) +{ + const gchar *name; + const gchar *pid; + FbData *fata = data; + gchar *tname; + PurpleAccount *acct; + + acct = purple_conversation_get_account(conv); + pid = purple_account_get_protocol_id(acct); + + if ((type == PURPLE_CONVERSATION_UPDATE_UNSEEN) && + purple_strequal(pid, FB_PROTOCOL_ID) && + purple_account_get_bool(acct, "mark-read", TRUE)) + { + /* Use event loop for purple_conversation_has_focus() */ + name = purple_conversation_get_name(conv); + tname = g_strconcat("conv-read-", name, NULL); + fb_data_add_timeout(fata, tname, 1, fb_cb_conv_read, conv); + g_free(tname); + } +} + +static void +fb_cb_conv_deleting(PurpleConversation *conv, gpointer data) +{ + const gchar *name; + const gchar *pid; + FbData *fata = data; + gchar *tname; + PurpleAccount *acct; + + acct = purple_conversation_get_account(conv); + pid = purple_account_get_protocol_id(acct); + + if (!purple_strequal(pid, FB_PROTOCOL_ID)) { + return; + } + + name = purple_conversation_get_name(conv); + tname = g_strconcat("conv-read-", name, NULL); + fb_data_clear_timeout(fata, tname, TRUE); + g_free(tname); +} + +static void +fb_blist_chat_create(GSList *buddies, gpointer data) +{ + const gchar *name; + FbApi *api; + FbData *fata = data; + FbId *did; + FbId uid; + GSList *l; + GSList *uids = NULL; + PurpleConnection *gc; + PurpleRequestCommonParameters *cpar; + + gc = fb_data_get_connection(fata); + api = fb_data_get_api(fata); + + if (g_slist_length(buddies) < 2) { + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Initiate Chat"), + _("Failed to Initiate Chat"), + _("At least two initial chat participants" + " are required."), + cpar); + return; + } + + for (l = buddies; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + uid = FB_ID_FROM_STR(name); + did = g_memdup(&uid, sizeof uid); + uids = g_slist_prepend(uids, did); + } + + fb_api_thread_create(api, uids); + g_slist_free_full(uids, g_free); +} + +static void +fb_blist_chat_init(PurpleBlistNode *node, gpointer data) +{ + FbData *fata = data; + GSList *select = NULL; + PurpleConnection *gc; + + if (!PURPLE_IS_BUDDY(node)) { + return; + } + + gc = fb_data_get_connection(fata); + select = g_slist_prepend(select, PURPLE_BUDDY(node)); + + fb_util_request_buddy(gc, + _("Initiate Chat"), + _("Initial Chat Participants"), + _("Select at least two initial participants."), + select, TRUE, + G_CALLBACK(fb_blist_chat_create), NULL, + fata); + g_slist_free(select); +} + +static void +fb_login(PurpleAccount *acct) +{ + const gchar *pass; + const gchar *user; + FbApi *api; + FbData *fata; + gpointer convh; + PurpleConnection *gc; + + gc = purple_account_get_connection(acct); + + fata = fb_data_new(gc); + api = fb_data_get_api(fata); + convh = purple_conversations_get_handle(); + purple_connection_set_protocol_data(gc, fata); + + g_signal_connect(api, + "auth", + G_CALLBACK(fb_cb_api_auth), + fata); + g_signal_connect(api, + "connect", + G_CALLBACK(fb_cb_api_connect), + fata); + g_signal_connect(api, + "contact", + G_CALLBACK(fb_cb_api_contact), + fata); + g_signal_connect(api, + "contacts", + G_CALLBACK(fb_cb_api_contacts), + fata); + g_signal_connect(api, + "contacts-delta", + G_CALLBACK(fb_cb_api_contacts_delta), + fata); + g_signal_connect(api, + "error", + G_CALLBACK(fb_cb_api_error), + fata); + g_signal_connect(api, + "events", + G_CALLBACK(fb_cb_api_events), + fata); + g_signal_connect(api, + "messages", + G_CALLBACK(fb_cb_api_messages), + fata); + g_signal_connect(api, + "presences", + G_CALLBACK(fb_cb_api_presences), + fata); + g_signal_connect(api, + "thread", + G_CALLBACK(fb_cb_api_thread), + fata); + g_signal_connect(api, + "thread-create", + G_CALLBACK(fb_cb_api_thread_create), + fata); + g_signal_connect(api, + "thread-kicked", + G_CALLBACK(fb_cb_api_thread_kicked), + fata); + g_signal_connect(api, + "threads", + G_CALLBACK(fb_cb_api_threads), + fata); + g_signal_connect(api, + "typing", + G_CALLBACK(fb_cb_api_typing), + fata); + + purple_signal_connect(convh, + "conversation-updated", + gc, + G_CALLBACK(fb_cb_conv_updated), + fata); + purple_signal_connect(convh, + "deleting-conversation", + gc, + G_CALLBACK(fb_cb_conv_deleting), + fata); + + if (!fb_data_load(fata) || !purple_account_get_remember_password(acct)) { + user = purple_account_get_username(acct); + pass = purple_connection_get_password(gc); + purple_connection_update_progress(gc, _("Authenticating"), + 1, 4); + fb_api_auth(api, user, pass); + return; + } + + purple_connection_update_progress(gc, _("Fetching contacts"), 2, 4); + fb_api_contacts(api); +} + +static void +fb_close(PurpleConnection *gc) +{ + FbApi *api; + FbData *fata; + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + fb_data_save(fata); + fb_api_disconnect(api); + g_object_unref(fata); + + purple_connection_set_protocol_data(gc, NULL); + purple_signals_disconnect_by_handle(gc); +} + +static GList * +fb_status_types(PurpleAccount *acct) +{ + PurpleStatusType *type; + GList *types = NULL; + + type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + /* Just a NULL state (as of now) for compatibility */ + type = purple_status_type_new(PURPLE_STATUS_AWAY, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + type = purple_status_type_new(PURPLE_STATUS_INVISIBLE, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + type = purple_status_type_new(PURPLE_STATUS_OFFLINE, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + return g_list_reverse(types); +} + +static const char * +fb_list_icon(PurpleAccount *account, PurpleBuddy *buddy) +{ + return "facebook"; +} + +static void +fb_client_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *info, + gboolean full) +{ + const gchar *name; + PurplePresence *pres; + PurpleStatus *status; + + pres = purple_buddy_get_presence(buddy); + status = purple_presence_get_active_status(pres); + + if (!PURPLE_BUDDY_IS_ONLINE(buddy)) { + /* Prevent doubles statues for Offline buddies */ + /* See: pidgin_get_tooltip_text() in gtkblist.c */ + purple_notify_user_info_remove_last_item(info); + } + + name = purple_status_get_name(status); + purple_notify_user_info_add_pair_plaintext(info, _("Status"), name); +} + +static GList * +fb_client_blist_node_menu(PurpleBlistNode *node) +{ + FbData *fata; + GList *acts = NULL; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleMenuAction *act; + + if (!PURPLE_IS_BUDDY(node)) { + return NULL; + } + + acct = purple_buddy_get_account(PURPLE_BUDDY(node)); + gc = purple_account_get_connection(acct); + fata = purple_connection_get_protocol_data(gc); + + act = purple_menu_action_new(_("Initiate _Chat"), + PURPLE_CALLBACK(fb_blist_chat_init), + fata, NULL); + acts = g_list_prepend(acts, act); + + return g_list_reverse(acts); +} + +static gboolean +fb_client_offline_message(const PurpleBuddy *buddy) +{ + return TRUE; +} + +static void +fb_server_set_status(PurpleAccount *acct, PurpleStatus *status) +{ + FbApi *api; + FbData *fata; + gboolean invis; + PurpleConnection *gc; + PurpleStatusPrimitive pstat; + PurpleStatusType *type; + + gc = purple_account_get_connection(acct); + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + type = purple_status_get_status_type(status); + pstat = purple_status_type_get_primitive(type); + invis = fb_api_is_invisible(api); + + if ((pstat == PURPLE_STATUS_INVISIBLE) && !invis) { + fb_api_connect(api, TRUE); + } else if ((pstat != PURPLE_STATUS_OFFLINE) && invis) { + fb_api_connect(api, FALSE); + } +} + +static gint +fb_im_send(PurpleConnection *gc, PurpleMessage *msg) +{ + const gchar *name; + const gchar *text; + FbApi *api; + FbData *fata; + FbId uid; + gchar *sext; + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + name = purple_message_get_recipient(msg); + uid = FB_ID_FROM_STR(name); + + text = purple_message_get_contents(msg); + sext = purple_markup_strip_html(text); + fb_api_message(api, uid, FALSE, sext); + g_free(sext); + return 1; +} + +static guint +fb_im_send_typing(PurpleConnection *gc, const gchar *name, + PurpleIMTypingState state) +{ + FbApi *api; + FbData *fata; + FbId uid; + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + uid = FB_ID_FROM_STR(name); + + fb_api_typing(api, uid, state != PURPLE_IM_NOT_TYPING); + return 0; +} + +static GList * +fb_chat_info() +{ + GList *pces = NULL; + PurpleProtocolChatEntry *pce; + + pce = g_new0(PurpleProtocolChatEntry, 1); + pce->label = _("Chat _Name:"); + pce->identifier = "name"; + pce->required = TRUE; + pces = g_list_prepend(pces, pce); + + return g_list_reverse(pces); +} + +static GHashTable * +fb_chat_info_defaults(PurpleConnection *gc, const gchar *name) +{ + GHashTable *data; + + data = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); + g_hash_table_insert(data, "name", g_strdup(name)); + + return data; +} + +static void +fb_chat_join(PurpleConnection *gc, GHashTable *data) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + gint id; + PurpleChatConversation *chat; + PurpleRequestCommonParameters *cpar; + + name = g_hash_table_lookup(data, "name"); + g_return_if_fail(name != NULL); + + if (!FB_ID_IS_STR(name)) { + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Join a Chat"), + _("Failed to Join Chat"), + _("Invalid Facebook identifier."), + cpar); + return; + } + + tid = FB_ID_FROM_STR(name); + id = fb_id_hash(&tid); + chat = purple_conversations_find_chat(gc, id); + + if ((chat != NULL) && !purple_chat_conversation_has_left(chat)) { + purple_conversation_present(PURPLE_CONVERSATION(chat)); + return; + } + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + fb_api_thread(api, tid); +} + +static gchar * +fb_chat_get_name(GHashTable *data) +{ + const gchar *name; + + name = g_hash_table_lookup(data, "name"); + g_return_val_if_fail(name != NULL, NULL); + + return g_strdup(name); +} + +static void +fb_chat_invite(PurpleConnection *gc, gint id, const gchar *msg, + const gchar *who) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + FbId uid; + PurpleChatConversation *chat; + PurpleRequestCommonParameters *cpar; + + if (!FB_ID_IS_STR(who)) { + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Invite Buddy Into Chat Room"), + _("Failed to Invite User"), + _("Invalid Facebook identifier."), + cpar); + return; + } + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + chat = purple_conversations_find_chat(gc, id); + + name = purple_conversation_get_name(PURPLE_CONVERSATION(chat)); + tid = FB_ID_FROM_STR(name); + uid = FB_ID_FROM_STR(who); + + fb_api_thread_invite(api, tid, uid); +} + +static gint +fb_chat_send(PurpleConnection *gc, gint id, PurpleMessage *msg) +{ + const gchar *name; + const gchar *text; + FbApi *api; + FbData *fata; + FbId tid; + gchar *sext; + PurpleAccount *acct; + PurpleChatConversation *chat; + + acct = purple_connection_get_account(gc); + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + chat = purple_conversations_find_chat(gc, id); + + name = purple_conversation_get_name(PURPLE_CONVERSATION(chat)); + tid = FB_ID_FROM_STR(name); + + text = purple_message_get_contents(msg); + sext = purple_markup_strip_html(text); + fb_api_message(api, tid, TRUE, sext); + g_free(sext); + + name = purple_account_get_username(acct); + purple_serv_got_chat_in(gc, id, name, + purple_message_get_flags(msg), + purple_message_get_contents(msg), + time(NULL)); + return 0; +} + +static void +fb_chat_set_topic(PurpleConnection *gc, gint id, const gchar *topic) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + PurpleChatConversation *chat; + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + chat = purple_conversations_find_chat(gc, id); + + name = purple_conversation_get_name(PURPLE_CONVERSATION(chat)); + tid = FB_ID_FROM_STR(name); + fb_api_thread_topic(api, tid, topic); +} + +static PurpleRoomlist * +fb_roomlist_get_list(PurpleConnection *gc) +{ + FbApi *api; + FbData *fata; + GList *flds = NULL; + PurpleAccount *acct; + PurpleRoomlist *list; + PurpleRoomlistField *fld; + + fata = purple_connection_get_protocol_data(gc); + list = fb_data_get_roomlist(fata); + g_return_val_if_fail(list == NULL, NULL); + + api = fb_data_get_api(fata); + acct = purple_connection_get_account(gc); + list = purple_roomlist_new(acct); + fb_data_set_roomlist(fata, list); + + fld = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, + _("Topic"), "topic", FALSE); + flds = g_list_prepend(flds, fld); + + fld = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, + _("Users"), "users", FALSE); + flds = g_list_prepend(flds, fld); + + flds = g_list_reverse(flds); + purple_roomlist_set_fields(list, flds); + + purple_roomlist_set_in_progress(list, TRUE); + fb_api_threads(api); + return list; +} + +static void +fb_roomlist_cancel(PurpleRoomlist *list) +{ + FbData *fata; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleRoomlist *cist; + + acct = purple_roomlist_get_account(list); + gc = purple_account_get_connection(acct); + fata = purple_connection_get_protocol_data(gc); + cist = fb_data_get_roomlist(fata); + + if (G_LIKELY(cist == list)) { + fb_data_set_roomlist(fata, NULL); + } + + purple_roomlist_set_in_progress(list, FALSE); + g_object_unref(list); +} + +static PurpleCmdRet +fb_cmd_kick(PurpleConversation *conv, const gchar *cmd, gchar **args, + gchar **error, gpointer data) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + FbId uid; + GError *err = NULL; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleChatConversation *chat; + + g_return_val_if_fail(PURPLE_IS_CHAT_CONVERSATION(conv), + PURPLE_CMD_RET_FAILED); + + gc = purple_conversation_get_connection(conv); + acct = purple_connection_get_account(gc); + chat = PURPLE_CHAT_CONVERSATION(conv); + bdy = fb_util_account_find_buddy(acct, chat, args[0], &err); + + if (err != NULL) { + *error = g_strdup_printf(_("%s."), err->message); + g_error_free(err); + return PURPLE_CMD_RET_FAILED; + } + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + name = purple_conversation_get_name(conv); + tid = FB_ID_FROM_STR(name); + + name = purple_buddy_get_name(bdy); + uid = FB_ID_FROM_STR(name); + + fb_api_thread_remove(api, tid, uid); + return PURPLE_CMD_RET_OK; +} + +static PurpleCmdRet +fb_cmd_leave(PurpleConversation *conv, const gchar *cmd, gchar **args, + gchar **error, gpointer data) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + gint id; + PurpleConnection *gc; + PurpleChatConversation *chat; + + g_return_val_if_fail(PURPLE_IS_CHAT_CONVERSATION(conv), + PURPLE_CMD_RET_FAILED); + + gc = purple_conversation_get_connection(conv); + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + chat = PURPLE_CHAT_CONVERSATION(conv); + id = purple_chat_conversation_get_id(chat); + + name = purple_conversation_get_name(conv); + tid = FB_ID_FROM_STR(name); + + purple_serv_got_chat_left(gc, id); + fb_api_thread_remove(api, tid, 0); + return PURPLE_CMD_RET_OK; +} + +static void +facebook_protocol_init(PurpleProtocol *protocol) +{ + GList *opts = NULL; + PurpleAccountOption *opt; + + protocol->id = FB_PROTOCOL_ID; + protocol->name = "Facebook"; + protocol->options = OPT_PROTO_CHAT_TOPIC; + + opt = purple_account_option_int_new(_("Buddy list sync interval"), + "sync-interval", 5); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Mark messages as read on focus"), + "mark-read", TRUE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Mark messages as read only when available"), + "mark-read-available", FALSE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Show self messages"), + "show-self", TRUE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Show unread messages"), + "show-unread", TRUE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Open new group chats with " + "incoming messages"), + "group-chat-open", TRUE); + opts = g_list_prepend(opts, opt); + protocol->account_options = g_list_reverse(opts); +} + +static void +facebook_protocol_class_init(PurpleProtocolClass *klass) +{ + klass->login = fb_login; + klass->close = fb_close; + klass->status_types = fb_status_types; + klass->list_icon = fb_list_icon; +} + +static void +facebook_protocol_client_iface_init(PurpleProtocolClientIface *iface) +{ + iface->tooltip_text = fb_client_tooltip_text; + iface->blist_node_menu = fb_client_blist_node_menu; + iface->offline_message = fb_client_offline_message; +} + +static void +facebook_protocol_server_iface_init(PurpleProtocolServerIface *iface) +{ + iface->set_status = fb_server_set_status; +} + +static void +facebook_protocol_im_iface_init(PurpleProtocolIMIface *iface) +{ + iface->send = fb_im_send; + iface->send_typing = fb_im_send_typing; +} + +static void +facebook_protocol_chat_iface_init(PurpleProtocolChatIface *iface) +{ + iface->info = fb_chat_info; + iface->info_defaults = fb_chat_info_defaults; + iface->join = fb_chat_join; + iface->get_name = fb_chat_get_name; + iface->invite = fb_chat_invite; + iface->send = fb_chat_send; + iface->set_topic = fb_chat_set_topic; +} + +static void +facebook_protocol_roomlist_iface_init(PurpleProtocolRoomlistIface *iface) +{ + iface->get_list = fb_roomlist_get_list; + iface->cancel = fb_roomlist_cancel; +} + +PURPLE_DEFINE_TYPE_EXTENDED( + FacebookProtocol, facebook_protocol, PURPLE_TYPE_PROTOCOL, 0, + + PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CLIENT_IFACE, + facebook_protocol_client_iface_init) + PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_SERVER_IFACE, + facebook_protocol_server_iface_init) + PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_IM_IFACE, + facebook_protocol_im_iface_init) + PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CHAT_IFACE, + facebook_protocol_chat_iface_init) + PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_ROOMLIST_IFACE, + facebook_protocol_roomlist_iface_init) +); + +static void +fb_cmds_register(void) +{ + PurpleCmdId id; + + static PurpleCmdFlag cflags = + PURPLE_CMD_FLAG_CHAT | + PURPLE_CMD_FLAG_PROTOCOL_ONLY; + + g_return_if_fail(fb_cmds == NULL); + + id = purple_cmd_register("kick", "s", PURPLE_CMD_P_PROTOCOL, cflags, + fb_protocol->id, fb_cmd_kick, + _("kick: Kick someone from the chat"), + NULL); + fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id)); + + id = purple_cmd_register("leave", "", PURPLE_CMD_P_PROTOCOL, cflags, + fb_protocol->id, fb_cmd_leave, + _("leave: Leave the chat"), + NULL); + fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id)); +} + +static void +fb_cmds_unregister_free(gpointer data) +{ + PurpleCmdId id = GPOINTER_TO_UINT(data); + purple_cmd_unregister(id); +} + +static void +fb_cmds_unregister(void) +{ + g_slist_free_full(fb_cmds, fb_cmds_unregister_free); +} + +static PurplePluginInfo * +plugin_query(GError **error) +{ + return purple_plugin_info_new( + "id", FB_PROTOCOL_ID, + "name", "Facebook Protocol", + "version", DISPLAY_VERSION, + "category", N_("Protocol"), + "summary", N_("Facebook Protocol Plugin"), + "description", N_("Facebook Protocol Plugin"), + "website", PURPLE_WEBSITE, + "abi-version", PURPLE_ABI_VERSION, + "flags", PURPLE_PLUGIN_INFO_FLAGS_INTERNAL | + PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD, + NULL + ); +} + +static gboolean +plugin_load(PurplePlugin *plugin, GError **error) +{ + facebook_protocol_register_type(plugin); + fb_protocol = purple_protocols_add(FACEBOOK_TYPE_PROTOCOL, error); + + if (fb_protocol == NULL) { + return FALSE; + } + + fb_cmds_register(); + return TRUE; +} + +static gboolean +plugin_unload(PurplePlugin *plugin, GError **error) +{ + fb_cmds_unregister(); + return purple_protocols_remove(fb_protocol, error); +} + +PURPLE_PLUGIN_INIT(facebook, plugin_query, plugin_load, plugin_unload); diff --git a/pidgin/libpurple/protocols/facebook/facebook.h b/pidgin/libpurple/protocols/facebook/facebook.h new file mode 100644 index 0000000..edc8b26 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/facebook.h @@ -0,0 +1,84 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_H_ +#define _FACEBOOK_H_ + +/** + * SECTION:facebook + * @section_id: facebook-plugin + * @short_description: <filename>facebook.h</filename> + * @title: Facebook Plugin + * + * The Facebook Messenger #PurpleProtocol. + */ + +#include <glib.h> +#include <gmodule.h> + +#define FACEBOOK_TYPE_PROTOCOL (facebook_protocol_get_type()) +#define FACEBOOK_PROTOCOL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FACEBOOK_TYPE_PROTOCOL, FacebookProtocol)) +#define FACEBOOK_PROTOCOL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FACEBOOK_TYPE_PROTOCOL, FacebookProtocolClass)) +#define FACEBOOK_IS_PROTOCOL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FACEBOOK_TYPE_PROTOCOL)) +#define FACEBOOK_IS_PROTOCOL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FACEBOOK_TYPE_PROTOCOL)) +#define FACEBOOK_PROTOCOL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FACEBOOK_TYPE_PROTOCOL, FacebookProtocolClass)) + +/** + * FB_PROTOCOL_ID: + * + * The Facebook protocol identifier. + */ +#define FB_PROTOCOL_ID "prpl-facebook" + +typedef struct _FacebookProtocol FacebookProtocol; +typedef struct _FacebookProtocolClass FacebookProtocolClass; + +/** + * FacebookProtocol: + * + * Represents the Facebook #PurpleProtocol. + */ +struct _FacebookProtocol +{ + /*< private >*/ + PurpleProtocol parent; +}; + +/** + * FacebookProtocolClass: + * + * The base class for all #FacebookProtocol's. + */ +struct _FacebookProtocolClass +{ + /*< private >*/ + PurpleProtocolClass parent_class; +}; + +/** + * facebook_protocol_get_type: + * + * Returns: The #GType for a #FacebookProtocol. + */ +G_MODULE_EXPORT GType +facebook_protocol_get_type(void); + +#endif /* _FACEBOOK_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/http.c b/pidgin/libpurple/protocols/facebook/http.c new file mode 100644 index 0000000..148c9c2 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/http.c @@ -0,0 +1,438 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include <string.h> + +#include "http.h" + +struct _FbHttpConns +{ + GHashTable *cons; + gboolean canceled; +}; + +GQuark +fb_http_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-http-error-quark"); + } + + return q; +} + +FbHttpConns * +fb_http_conns_new(void) +{ + FbHttpConns *cons; + + cons = g_new0(FbHttpConns, 1); + cons->cons = g_hash_table_new(g_direct_hash, g_direct_equal); + return cons; +} + +void +fb_http_conns_free(FbHttpConns *cons) +{ + g_return_if_fail(cons != NULL); + + g_hash_table_destroy(cons->cons); + g_free(cons); +} + +void +fb_http_conns_cancel_all(FbHttpConns *cons) +{ + GHashTableIter iter; + gpointer con; + + g_return_if_fail(cons != NULL); + g_return_if_fail(!cons->canceled); + + cons->canceled = TRUE; + g_hash_table_iter_init(&iter, cons->cons); + + while (g_hash_table_iter_next(&iter, &con, NULL)) { + g_hash_table_iter_remove(&iter); + purple_http_conn_cancel(con); + } +} + +gboolean +fb_http_conns_is_canceled(FbHttpConns *cons) +{ + g_return_val_if_fail(cons != NULL, TRUE); + return cons->canceled; +} + +void +fb_http_conns_add(FbHttpConns *cons, PurpleHttpConnection *con) +{ + g_return_if_fail(cons != NULL); + g_return_if_fail(!cons->canceled); + g_hash_table_replace(cons->cons, con, con); +} + +void +fb_http_conns_remove(FbHttpConns *cons, PurpleHttpConnection *con) +{ + g_return_if_fail(cons != NULL); + g_return_if_fail(!cons->canceled); + g_hash_table_remove(cons->cons, con); +} + +void +fb_http_conns_reset(FbHttpConns *cons) +{ + g_return_if_fail(cons != NULL); + cons->canceled = FALSE; + g_hash_table_remove_all(cons->cons); +} + +gboolean +fb_http_error_chk(PurpleHttpResponse *res, GError **error) +{ + const gchar *msg; + gint code; + + if (purple_http_response_is_successful(res)) { + return TRUE; + } + + msg = purple_http_response_get_error(res); + code = purple_http_response_get_code(res); + g_set_error(error, FB_HTTP_ERROR, code, "%s", msg); + return FALSE; +} + +FbHttpParams * +fb_http_params_new(void) +{ + return g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); +} + +FbHttpParams * +fb_http_params_new_parse(const gchar *data, gboolean isurl) +{ + const gchar *tail; + gchar *key; + gchar **ps; + gchar *val; + guint i; + FbHttpParams *params; + + params = fb_http_params_new(); + + if (data == NULL) { + return params; + } + + if (isurl) { + data = strchr(data, '?'); + + if (data == NULL) { + return params; + } + + tail = strchr(++data, '#'); + + if (tail != NULL) { + data = g_strndup(data, tail - data); + } else { + data = g_strdup(data); + } + } + + ps = g_strsplit(data, "&", 0); + + for (i = 0; ps[i] != NULL; i++) { + key = ps[i]; + val = strchr(ps[i], '='); + + if (val == NULL) { + continue; + } + + *(val++) = 0; + key = g_uri_unescape_string(key, NULL); + val = g_uri_unescape_string(val, NULL); + g_hash_table_replace(params, key, val); + } + + if (isurl) { + g_free((gchar *) data); + } + + g_strfreev(ps); + return params; +} + +void +fb_http_params_free(FbHttpParams *params) +{ + g_hash_table_destroy(params); +} + +gchar * +fb_http_params_close(FbHttpParams *params, const gchar *url) +{ + GHashTableIter iter; + gpointer key; + gpointer val; + GString *ret; + + g_hash_table_iter_init(&iter, params); + ret = g_string_new(NULL); + + while (g_hash_table_iter_next(&iter, &key, &val)) { + if (val == NULL) { + g_hash_table_iter_remove(&iter); + continue; + } + + if (ret->len > 0) { + g_string_append_c(ret, '&'); + } + + g_string_append_uri_escaped(ret, key, NULL, TRUE); + g_string_append_c(ret, '='); + g_string_append_uri_escaped(ret, val, NULL, TRUE); + } + + if (url != NULL) { + g_string_prepend_c(ret, '?'); + g_string_prepend(ret, url); + } + + fb_http_params_free(params); + return g_string_free(ret, FALSE); +} + +static const gchar * +fb_http_params_get(FbHttpParams *params, const gchar *name, GError **error) +{ + const gchar *ret; + + ret = g_hash_table_lookup(params, name); + + if (ret == NULL) { + g_set_error(error, FB_HTTP_ERROR, FB_HTTP_ERROR_NOMATCH, + _("No matches for %s"), name); + return NULL; + } + + return ret; +} + +gboolean +fb_http_params_get_bool(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *val; + + val = fb_http_params_get(params, name, error); + + if (val == NULL) { + return FALSE; + } + + return g_ascii_strcasecmp(val, "TRUE") == 0; +} + +gdouble +fb_http_params_get_dbl(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *val; + + val = fb_http_params_get(params, name, error); + + if (val == NULL) { + return 0.0; + } + + return g_ascii_strtod(val, NULL); +} + +gint64 +fb_http_params_get_int(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *val; + + val = fb_http_params_get(params, name, error); + + if (val == NULL) { + return 0; + } + + return g_ascii_strtoll(val, NULL, 10); +} + +const gchar * +fb_http_params_get_str(FbHttpParams *params, const gchar *name, + GError **error) +{ + return fb_http_params_get(params, name, error); +} + +gchar * +fb_http_params_dup_str(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *str; + + str = fb_http_params_get(params, name, error); + return g_strdup(str); +} + +static void +fb_http_params_set(FbHttpParams *params, const gchar *name, gchar *value) +{ + gchar *key; + + key = g_strdup(name); + g_hash_table_replace(params, key, value); +} + +void +fb_http_params_set_bool(FbHttpParams *params, const gchar *name, + gboolean value) +{ + gchar *val; + + val = g_strdup(value ? "true" : "false"); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_dbl(FbHttpParams *params, const gchar *name, gdouble value) +{ + gchar *val; + + val = g_strdup_printf("%f", value); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_int(FbHttpParams *params, const gchar *name, gint64 value) +{ + gchar *val; + + val = g_strdup_printf("%" G_GINT64_FORMAT, value); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_str(FbHttpParams *params, const gchar *name, + const gchar *value) +{ + gchar *val; + + val = g_strdup(value); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_strf(FbHttpParams *params, const gchar *name, + const gchar *format, ...) +{ + gchar *val; + va_list ap; + + va_start(ap, format); + val = g_strdup_vprintf(format, ap); + va_end(ap); + + fb_http_params_set(params, name, val); +} + +gboolean +fb_http_urlcmp(const gchar *url1, const gchar *url2, gboolean protocol) +{ + const gchar *str1; + const gchar *str2; + gboolean ret = TRUE; + gint int1; + gint int2; + guint i; + PurpleHttpURL *purl1; + PurpleHttpURL *purl2; + + static const const gchar * (*funcs[]) (const PurpleHttpURL *url) = { + /* Always first so it can be skipped */ + purple_http_url_get_protocol, + + purple_http_url_get_fragment, + purple_http_url_get_host, + purple_http_url_get_password, + purple_http_url_get_path, + purple_http_url_get_username + }; + + if ((url1 == NULL) || (url2 == NULL)) { + return url1 == url2; + } + + if (strstr(url1, url2) != NULL || strstr(url2, url1) != NULL) { + return TRUE; + } + + purl1 = purple_http_url_parse(url1); + + if (purl1 == NULL) { + return g_ascii_strcasecmp(url1, url2) == 0; + } + + purl2 = purple_http_url_parse(url2); + + if (purl2 == NULL) { + purple_http_url_free(purl1); + return g_ascii_strcasecmp(url1, url2) == 0; + } + + for (i = protocol ? 0 : 1; i < G_N_ELEMENTS(funcs); i++) { + str1 = funcs[i](purl1); + str2 = funcs[i](purl2); + + if (!purple_strequal(str1, str2)) { + ret = FALSE; + break; + } + } + + if (ret && protocol) { + int1 = purple_http_url_get_port(purl1); + int2 = purple_http_url_get_port(purl2); + + if (int1 != int2) { + ret = FALSE; + } + } + + purple_http_url_free(purl1); + purple_http_url_free(purl2); + return ret; +} diff --git a/pidgin/libpurple/protocols/facebook/http.h b/pidgin/libpurple/protocols/facebook/http.h new file mode 100644 index 0000000..8419dda --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/http.h @@ -0,0 +1,371 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_HTTP_H_ +#define _FACEBOOK_HTTP_H_ + +/** + * SECTION:http + * @section_id: facebook-http + * @short_description: <filename>http.h</filename> + * @title: HTTP Utilities + * + * The HTTP utilities. + */ + +#include <glib.h> + +#include <libpurple/http.h> + +/** + * FB_HTTP_ERROR: + * + * The #GQuark of the domain of HTTP errors. + */ +#define FB_HTTP_ERROR fb_http_error_quark() + +/** + * FbHttpConns: + * + * Represents a set of #PurpleHttpConnection. + */ +typedef struct _FbHttpConns FbHttpConns; + +/** + * FbHttpParams: + * + * Represents a set of key/value HTTP parameters. + */ +typedef GHashTable FbHttpParams; + +/** + * FbHttpError: + * @FB_HTTP_ERROR_SUCCESS: There is no error. + * @FB_HTTP_ERROR_NOMATCH: The name does not match anything. + * + * The error codes for the #FB_HTTP_ERROR domain. + */ +typedef enum +{ + FB_HTTP_ERROR_SUCCESS = 0, + FB_HTTP_ERROR_NOMATCH +} FbHttpError; + +/** + * fb_http_error_quark: + * + * Gets the #GQuark of the domain of HTTP errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_http_error_quark(void); + +/** + * fb_http_conns_new: + * + * Creates a new #FbHttpConns. The returned #FbHttpConns should be + * freed with #fb_http_conns_free() when no longer needed. + * + * Returns: The new #FbHttpConns. + */ +FbHttpConns * +fb_http_conns_new(void); + +/** + * fb_http_conns_free: + * @cons: The #FbHttpConns. + * + * Frees all memory used by the #FbHttpConns. This will *not* cancel + * the any of the added #PurpleHttpConnection. + */ +void +fb_http_conns_free(FbHttpConns *cons); + +/** + * fb_http_conns_cancel_all: + * @cons: The #FbHttpConns. + * + * Cancels each #PurpleHttpConnection in the #FbHttpConns. + */ +void +fb_http_conns_cancel_all(FbHttpConns *cons); + +/** + * fb_http_conns_is_canceled: + * @cons: The #FbHttpConns. + * + * Determines if the #FbHttpConns has been canceled. + * + * Returns: #TRUE if it has been canceled, otherwise #FALSE. + */ +gboolean +fb_http_conns_is_canceled(FbHttpConns *cons); + +/** + * fb_http_conns_add: + * @cons: The #FbHttpConns. + * @con: The #PurpleHttpConnection. + * + * Adds a #PurpleHttpConnection to the #FbHttpConns. + */ +void +fb_http_conns_add(FbHttpConns *cons, PurpleHttpConnection *con); + +/** + * fb_http_conns_remove: + * @cons: The #FbHttpConns. + * @con: The #PurpleHttpConnection. + * + * Removes a #PurpleHttpConnection from the #FbHttpConns. + */ +void +fb_http_conns_remove(FbHttpConns *cons, PurpleHttpConnection *con); + +/** + * fb_http_conns_reset: + * @cons: The #FbHttpConns. + * + * Resets the #FbHttpConns. This removes each #PurpleHttpConnection + * from the #FbHttpConns *without* canceling it. This allows the the + * #FbHttpConns to be reused. + */ +void +fb_http_conns_reset(FbHttpConns *cons); + +/** + * fb_http_error_chk: + * @res: The #PurpleHttpResponse. + * @error: The return location for the #GError or #NULL. + * + * Checks a #PurpleHttpResponse for success. This optionally assigns an + * appropriate #GError upon failure. + * + * Returns: #TRUE if the request was successful, otherwise #FALSE. + */ +gboolean +fb_http_error_chk(PurpleHttpResponse *res, GError **error); + +/** + * fb_http_params_new: + * + * Creates a new #FbHttpParams. The returned #FbHttpParams should be + * freed with #fb_http_params_free() when no longer needed. Optionally, + * instead of freeing, the returned #FbHttpParams can be closed with + * #fb_http_params_close(). + * + * Returns: The new #FbHttpParams. + */ +FbHttpParams * +fb_http_params_new(void); + +/** + * fb_http_params_new_parse: + * @data: The string containing HTTP parameters. + * @isurl: #TRUE if @data is a URL, otherwise #FALSE. + * + * Creates a new #FbHttpParams. This parses the #FbHttpParams from a + * string, which can be a URL. The returned #FbHttpParams should be + * freed with #fb_http_params_free() when no longer needed. Optionally, + * instead of freeing, the returned #FbHttpParams can be closed with + * #fb_http_params_close(). + * + * Returns: The new #FbHttpParams. + */ +FbHttpParams * +fb_http_params_new_parse(const gchar *data, gboolean isurl); + +/** + * fb_http_params_free: + * @params: The #FbHttpParams. + * + * Frees all memory used by the #FbHttpParams. + */ +void +fb_http_params_free(FbHttpParams *params); + +/** + * fb_http_params_close: + * @params: The #FbHttpParams. + * @url: The URL or #NULL. + * + * Closes the #FbHttpParams by returning a string representing the HTTP + * parameters. If @url is non-#NULL, then the parameters are appended + * to the value of @url. This frees the #FbHttpParams. The returned + * string should be freed with #g_free() when no longer needed. + * + * Returns: The string representation of the HTTP parameters. + */ +gchar * +fb_http_params_close(FbHttpParams *params, const gchar *url); + +/** + * fb_http_params_get_bool: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a boolean value from the #FbHttpParams. This optionally assigns + * an appropriate #GError upon failure. + * + * Return: The boolean value. + */ +gboolean +fb_http_params_get_bool(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_get_dbl: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a floating point value from the #FbHttpParams. This optionally + * assigns an appropriate #GError upon failure. + * + * Return: The floating point value. + */ +gdouble +fb_http_params_get_dbl(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_get_int: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets an integer value from the #FbHttpParams. This optionally + * assigns an appropriate #GError upon failure. + * + * Return: The integer value. + */ +gint64 +fb_http_params_get_int(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_get_str: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a string value from the #FbHttpParams. This optionally assigns + * an appropriate #GError upon failure. + * + * Return: The string value. + */ +const gchar * +fb_http_params_get_str(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_dup_str: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a duplicated string value from the #FbHttpParams. This + * optionally assigns an appropriate #GError upon failure. The returned + * string should be freed with #g_free() when no longer needed. + * + * Return: The duplicated string value. + */ +gchar * +fb_http_params_dup_str(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_set_bool: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets a boolean value to the #FbHttpParams. + */ +void +fb_http_params_set_bool(FbHttpParams *params, const gchar *name, + gboolean value); + +/** + * fb_http_params_set_dbl: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets a floating point value to the #FbHttpParams. + */ +void +fb_http_params_set_dbl(FbHttpParams *params, const gchar *name, gdouble value); + +/** + * fb_http_params_set_int: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets an integer value to the #FbHttpParams. + */ +void +fb_http_params_set_int(FbHttpParams *params, const gchar *name, gint64 value); + +/** + * fb_http_params_set_str: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets a string value to the #FbHttpParams. + */ +void +fb_http_params_set_str(FbHttpParams *params, const gchar *name, + const gchar *value); + +/** + * fb_http_params_set_strf: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Sets a formatted string value to the #FbHttpParams. + */ +void +fb_http_params_set_strf(FbHttpParams *params, const gchar *name, + const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_http_urlcmp: + * @url1: The first URL. + * @url2: The second URL. + * @protocol: #TRUE to match the protocols, otherwise #FALSE. + * + * Compares two URLs. This is more reliable than just comparing two URL + * strings, as it avoids casing in some areas, while not in others. It + * can also, optionally, ignore the matching of the URL protocol. + * + * Returns: #TRUE if the URLs match, otherwise #FALSE. + */ +gboolean +fb_http_urlcmp(const gchar *url1, const gchar *url2, gboolean protocol); + +#endif /* _FACEBOOK_HTTP_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/id.h b/pidgin/libpurple/protocols/facebook/id.h new file mode 100644 index 0000000..00b4bd7 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/id.h @@ -0,0 +1,131 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_ID_H_ +#define _FACEBOOK_ID_H_ + +/** + * SECTION:id + * @section_id: facebook-id + * @short_description: <filename>id.h</filename> + * @title: Facebook Identifier + * + * The Facebook identifier utilities. + */ + +#include <glib.h> +#include <glib/gprintf.h> + +#include "util.h" + +/** + * FB_ID_FORMAT: + * + * The format specifier for printing and scanning an #FbId. + */ +#define FB_ID_FORMAT G_GINT64_FORMAT + +/** + * FB_ID_MODIFIER: + * + * The length modifier for printing an #FbId. + */ +#define FB_ID_MODIFIER G_GINT64_MODIFIER + +/** + * FB_ID_STRMAX: + * + * The maximum length, including a null-terminating character, of the + * string representation of an #FbId. + */ +#define FB_ID_STRMAX 21 + +/** + * FB_TYPE_ID: + * + * The #GType of an #FbId. + */ +#define FB_TYPE_ID G_TYPE_INT64 + +/** + * FB_ID_CONSTANT: + * @v: The value. + * + * Inserts a literal #FbId into source code. + * + * Return: The literal #FbId value. + */ +#define FB_ID_CONSTANT(v) G_GINT64_CONSTANT(v) + +/** + * FB_ID_FROM_STR: + * @s: The string value. + * + * Converts a string to an #FbId. + * + * Return: The converted #FbId value. + */ +#define FB_ID_FROM_STR(s) g_ascii_strtoll(s, NULL, 10) + +/** + * FB_ID_IS_STR: + * @s: The string value. + * + * Determines if a string is an #FbId. + * + * Return: #TRUE if the string is an #FbId, otherwise #FALSE. + */ +#define FB_ID_IS_STR(s) fb_util_strtest(s, G_ASCII_DIGIT) + +/** + * FB_ID_TO_STR: + * @i: The #FbId. + * @s: The string buffer. + * + * Converts an #FbId to a string. The buffer should be at least the + * size of #FB_ID_STRMAX. + * + * Return: The converted string value. + */ +#define FB_ID_TO_STR(i, s) g_sprintf(s, "%" FB_ID_FORMAT, (FbId) i) + +/** + * fb_id_equal: + * + * Compares the values of two #FbId's for equality. See #g_int64_equal. + */ +#define fb_id_equal g_int64_equal + +/** + * fb_id_hash: + * + * Converts a pointer to a #FbId hash value. See #g_int64_hash. + */ +#define fb_id_hash g_int64_hash + +/** + * FbId: + * + * Represents a numeric Facebook identifier. + */ +typedef gint64 FbId; + +#endif /* _FACEBOOK_ID_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/json.c b/pidgin/libpurple/protocols/facebook/json.c new file mode 100644 index 0000000..f36839c --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/json.c @@ -0,0 +1,676 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include <stdarg.h> +#include <string.h> + +#include "json.h" +#include "util.h" + +typedef struct _FbJsonValue FbJsonValue; + +struct _FbJsonValue +{ + const gchar *expr; + FbJsonType type; + gboolean required; + GValue value; +}; + +struct _FbJsonValuesPrivate +{ + JsonNode *root; + GQueue *queue; + GList *next; + + gboolean isarray; + JsonArray *array; + guint index; + + GError *error; +}; + +G_DEFINE_TYPE(FbJsonValues, fb_json_values, G_TYPE_OBJECT); + +static void +fb_json_values_dispose(GObject *obj) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv = FB_JSON_VALUES(obj)->priv; + + while (!g_queue_is_empty(priv->queue)) { + value = g_queue_pop_head(priv->queue); + + if (G_IS_VALUE(&value->value)) { + g_value_unset(&value->value); + } + + g_free(value); + } + + if (priv->array != NULL) { + json_array_unref(priv->array); + } + + if (priv->error != NULL) { + g_error_free(priv->error); + } + + g_queue_free(priv->queue); +} + +static void +fb_json_values_class_init(FbJsonValuesClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_json_values_dispose; + g_type_class_add_private(klass, sizeof (FbJsonValuesPrivate)); +} + +static void +fb_json_values_init(FbJsonValues *values) +{ + FbJsonValuesPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(values, FB_TYPE_JSON_VALUES, + FbJsonValuesPrivate); + values->priv = priv; + + priv->queue = g_queue_new(); +} + +GQuark +fb_json_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-json-error-quark"); + } + + return q; +} + +JsonBuilder * +fb_json_bldr_new(JsonNodeType type) +{ + JsonBuilder *bldr; + + bldr = json_builder_new(); + + switch (type) { + case JSON_NODE_ARRAY: + fb_json_bldr_arr_begin(bldr, NULL); + break; + + case JSON_NODE_OBJECT: + fb_json_bldr_obj_begin(bldr, NULL); + break; + + default: + break; + } + + return bldr; +} + +gchar * +fb_json_bldr_close(JsonBuilder *bldr, JsonNodeType type, gsize *size) +{ + gchar *ret; + JsonGenerator *genr; + JsonNode *root; + + switch (type) { + case JSON_NODE_ARRAY: + fb_json_bldr_arr_end(bldr); + break; + + case JSON_NODE_OBJECT: + fb_json_bldr_obj_end(bldr); + break; + + default: + break; + } + + genr = json_generator_new(); + root = json_builder_get_root(bldr); + + json_generator_set_root(genr, root); + ret = json_generator_to_data(genr, size); + + json_node_free(root); + g_object_unref(genr); + g_object_unref(bldr); + + return ret; +} + +void +fb_json_bldr_arr_begin(JsonBuilder *bldr, const gchar *name) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_begin_array(bldr); +} + +void +fb_json_bldr_arr_end(JsonBuilder *bldr) +{ + json_builder_end_array(bldr); +} + +void +fb_json_bldr_obj_begin(JsonBuilder *bldr, const gchar *name) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_begin_object(bldr); +} + +void +fb_json_bldr_obj_end(JsonBuilder *bldr) +{ + json_builder_end_object(bldr); +} + +void +fb_json_bldr_add_bool(JsonBuilder *bldr, const gchar *name, gboolean value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_boolean_value(bldr, value); +} + +void +fb_json_bldr_add_dbl(JsonBuilder *bldr, const gchar *name, gdouble value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_double_value(bldr, value); +} + +void +fb_json_bldr_add_int(JsonBuilder *bldr, const gchar *name, gint64 value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_int_value(bldr, value); +} + +void +fb_json_bldr_add_str(JsonBuilder *bldr, const gchar *name, const gchar *value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_string_value(bldr, value); +} + +void +fb_json_bldr_add_strf(JsonBuilder *bldr, const gchar *name, + const gchar *format, ...) +{ + gchar *value; + va_list ap; + + va_start(ap, format); + value = g_strdup_vprintf(format, ap); + va_end(ap); + + fb_json_bldr_add_str(bldr, name, value); + g_free(value); +} + +JsonNode * +fb_json_node_new(const gchar *data, gssize size, GError **error) +{ + gchar *slice; + JsonNode *root; + JsonParser *prsr; + + g_return_val_if_fail(data != NULL, NULL); + + if (size < 0) { + size = strlen(data); + } + + /* Ensure data is null terminated for json-glib < 1.0.2 */ + slice = g_strndup(data, size); + prsr = json_parser_new(); + + if (!json_parser_load_from_data(prsr, slice, size, error)) { + g_object_unref(prsr); + g_free(slice); + return NULL; + } + + root = json_parser_get_root(prsr); + root = json_node_copy(root); + + g_object_unref(prsr); + g_free(slice); + return root; +} + +JsonNode * +fb_json_node_get(JsonNode *root, const gchar *expr, GError **error) +{ + GError *err = NULL; + guint size; + JsonArray *rslt; + JsonNode *node; + JsonNode *ret; + + /* Special case for json-glib < 0.99.2 */ + if (purple_strequal(expr, "$")) { + return json_node_copy(root); + } + + node = json_path_query(expr, root, &err); + + if (err != NULL) { + g_propagate_error(error, err); + json_node_free(node); + return NULL; + } + + rslt = json_node_get_array(node); + size = json_array_get_length(rslt); + + if (size < 1) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_NOMATCH, + _("No matches for %s"), expr); + json_node_free(node); + return NULL; + } + + if (size > 1) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_AMBIGUOUS, + _("Ambiguous matches for %s"), expr); + json_node_free(node); + return NULL; + } + + if (json_array_get_null_element(rslt, 0)) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_NULL, + _("Null value for %s"), expr); + json_node_free(node); + return NULL; + } + + ret = json_array_dup_element(rslt, 0); + json_node_free(node); + return ret; +} + +JsonNode * +fb_json_node_get_nth(JsonNode *root, guint n) +{ + GList *vals; + JsonNode *ret; + JsonObject *obj; + + obj = json_node_get_object(root); + vals = json_object_get_values(obj); + ret = g_list_nth_data(vals, n); + + g_list_free(vals); + return ret; +} + +JsonArray * +fb_json_node_get_arr(JsonNode *root, const gchar *expr, GError **error) +{ + JsonArray *ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return NULL; + } + + ret = json_node_dup_array(rslt); + json_node_free(rslt); + return ret; +} + +gboolean +fb_json_node_get_bool(JsonNode *root, const gchar *expr, GError **error) +{ + gboolean ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return FALSE; + } + + ret = json_node_get_boolean(rslt); + json_node_free(rslt); + return ret; +} + +gdouble +fb_json_node_get_dbl(JsonNode *root, const gchar *expr, GError **error) +{ + gdouble ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return 0.0; + } + + ret = json_node_get_double(rslt); + json_node_free(rslt); + return ret; +} + +gint64 +fb_json_node_get_int(JsonNode *root, const gchar *expr, GError **error) +{ + gint64 ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return 0; + } + + ret = json_node_get_int(rslt); + json_node_free(rslt); + return ret; +} + +gchar * +fb_json_node_get_str(JsonNode *root, const gchar *expr, GError **error) +{ + gchar *ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return NULL; + } + + ret = json_node_dup_string(rslt); + json_node_free(rslt); + return ret; +} + +FbJsonValues * +fb_json_values_new(JsonNode *root) +{ + FbJsonValues *values; + FbJsonValuesPrivate *priv; + + g_return_val_if_fail(root != NULL, NULL); + + values = g_object_new(FB_TYPE_JSON_VALUES, NULL); + priv = values->priv; + priv->root = root; + + return values; +} + +void +fb_json_values_add(FbJsonValues *values, FbJsonType type, gboolean required, + const gchar *expr) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv; + + g_return_if_fail(values != NULL); + g_return_if_fail(expr != NULL); + priv = values->priv; + + value = g_new0(FbJsonValue, 1); + value->expr = expr; + value->type = type; + value->required = required; + + g_queue_push_tail(priv->queue, value); +} + +JsonNode * +fb_json_values_get_root(FbJsonValues *values) +{ + FbJsonValuesPrivate *priv; + guint index; + + g_return_val_if_fail(values != NULL, NULL); + priv = values->priv; + + if (priv->array == NULL) { + return priv->root; + } + + g_return_val_if_fail(priv->index > 0, NULL); + index = priv->index - 1; + + if (json_array_get_length(priv->array) <= index) { + return NULL; + } + + return json_array_get_element(priv->array, index); +} + +void +fb_json_values_set_array(FbJsonValues *values, gboolean required, + const gchar *expr) +{ + FbJsonValuesPrivate *priv; + + g_return_if_fail(values != NULL); + priv = values->priv; + + priv->array = fb_json_node_get_arr(priv->root, expr, &priv->error); + priv->isarray = TRUE; + + if ((priv->error != NULL) && !required) { + g_clear_error(&priv->error); + } +} + +gboolean +fb_json_values_update(FbJsonValues *values, GError **error) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv; + GError *err = NULL; + GList *l; + GType type; + JsonNode *root; + JsonNode *node; + + g_return_val_if_fail(values != NULL, FALSE); + priv = values->priv; + + if (G_UNLIKELY(priv->error != NULL)) { + g_propagate_error(error, priv->error); + priv->error = NULL; + return FALSE; + } + + if (priv->isarray) { + if ((priv->array == NULL) || + (json_array_get_length(priv->array) <= priv->index)) + { + return FALSE; + } + + root = json_array_get_element(priv->array, priv->index++); + } else { + root = priv->root; + } + + g_return_val_if_fail(root != NULL, FALSE); + + for (l = priv->queue->head; l != NULL; l = l->next) { + value = l->data; + node = fb_json_node_get(root, value->expr, &err); + + if (G_IS_VALUE(&value->value)) { + g_value_unset(&value->value); + } + + if (err != NULL) { + json_node_free(node); + + if (value->required) { + g_propagate_error(error, err); + return FALSE; + } + + g_clear_error(&err); + continue; + } + + type = json_node_get_value_type(node); + + if (G_UNLIKELY(type != value->type)) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_TYPE, + _("Expected a %s but got a %s for %s"), + g_type_name(value->type), + g_type_name(type), + value->expr); + json_node_free(node); + return FALSE; + } + + json_node_get_value(node, &value->value); + json_node_free(node); + } + + priv->next = priv->queue->head; + return TRUE; +} + +const GValue * +fb_json_values_next(FbJsonValues *values) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv; + + g_return_val_if_fail(values != NULL, NULL); + priv = values->priv; + + g_return_val_if_fail(priv->next != NULL, NULL); + value = priv->next->data; + priv->next = priv->next->next; + + if (!G_IS_VALUE(&value->value)) { + return NULL; + } + + return &value->value; +} + +gboolean +fb_json_values_next_bool(FbJsonValues *values, gboolean defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_boolean(value); +} + +gdouble +fb_json_values_next_dbl(FbJsonValues *values, gdouble defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_double(value); +} + +gint64 +fb_json_values_next_int(FbJsonValues *values, gint64 defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_int64(value); +} + +const gchar * +fb_json_values_next_str(FbJsonValues *values, const gchar *defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_string(value); +} + +gchar * +fb_json_values_next_str_dup(FbJsonValues *values, const gchar *defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return g_strdup(defval); + } + + return g_value_dup_string(value); +} diff --git a/pidgin/libpurple/protocols/facebook/json.h b/pidgin/libpurple/protocols/facebook/json.h new file mode 100644 index 0000000..2a46216 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/json.h @@ -0,0 +1,517 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_JSON_H_ +#define _FACEBOOK_JSON_H_ + +/** + * SECTION:json + * @section_id: facebook-json + * @short_description: <filename>json.h</filename> + * @title: JSON Utilities + * + * The JSON utilities. + */ + +#include <glib.h> +#include <json-glib/json-glib.h> + +#define FB_TYPE_JSON_VALUES (fb_json_values_get_type()) +#define FB_JSON_VALUES(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_JSON_VALUES, FbJsonValues)) +#define FB_JSON_VALUES_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_JSON_VALUES, FbJsonValuesClass)) +#define FB_IS_JSON_VALUES(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_JSON_VALUES)) +#define FB_IS_JSON_VALUES_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_JSON_VALUES)) +#define FB_JSON_VALUES_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_JSON_VALUES, FbJsonValuesClass)) + +/** + * FB_JSON_ERROR: + * + * The #GQuark of the domain of JSON errors. + */ +#define FB_JSON_ERROR fb_json_error_quark() + +typedef struct _FbJsonValues FbJsonValues; +typedef struct _FbJsonValuesClass FbJsonValuesClass; +typedef struct _FbJsonValuesPrivate FbJsonValuesPrivate; + +/** + * FbJsonError: + * @FB_JSON_ERROR_SUCCESS: There is no error. + * @FB_JSON_ERROR_AMBIGUOUS: The node has ambiguous matches. + * @FB_JSON_ERROR_GENERAL: General failure. + * @FB_JSON_ERROR_NOMATCH: The node does not match anything. + * @FB_JSON_ERROR_NULL: The node is of type NULL. + * @FB_JSON_ERROR_TYPE: The node has an unexpected type. + * + * The error codes for the #FB_JSON_ERROR domain. + */ +typedef enum +{ + FB_JSON_ERROR_SUCCESS = 0, + FB_JSON_ERROR_AMBIGUOUS, + FB_JSON_ERROR_GENERAL, + FB_JSON_ERROR_NOMATCH, + FB_JSON_ERROR_NULL, + FB_JSON_ERROR_TYPE +} FbJsonError; + +/** + * FbJsonType: + * @FB_JSON_TYPE_NULL: An unknown value. + * @FB_JSON_TYPE_BOOL: A boolean (#TRUE or #FALSE). + * @FB_JSON_TYPE_DBL: A floating point number. + * @FB_JSON_TYPE_INT: A signed integer. + * @FB_JSON_TYPE_STR: A string. + * + * The JSON data types. + */ +typedef enum +{ + FB_JSON_TYPE_NULL = 0, + FB_JSON_TYPE_BOOL = G_TYPE_BOOLEAN, + FB_JSON_TYPE_DBL = G_TYPE_DOUBLE, + FB_JSON_TYPE_INT = G_TYPE_INT64, + FB_JSON_TYPE_STR = G_TYPE_STRING +} FbJsonType; + +/** + * FbJsonValues: + * + * Represents a JSON value handler. + */ +struct _FbJsonValues +{ + /*< private >*/ + GObject parent; + FbJsonValuesPrivate *priv; +}; + +/** + * FbJsonValuesClass: + * + * The base class for all #FbJsonValues's. + */ +struct _FbJsonValuesClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_json_values_get_type: + * + * Returns: The #GType for an #FbJsonValues. + */ +GType +fb_json_values_get_type(void); + +/** + * fb_json_error_quark: + * + * Gets the #GQuark of the domain of JSON errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_json_error_quark(void); + +/** + * fb_json_bldr_new: + * @type: The starting #JsonNodeType. + * + * Creates a new #JsonBuilder. The starting #JsonNodeType is likely to + * be #JSON_NODE_OBJECT. The returned #JsonBuilder should be freed with + * #g_object_unref() when no longer needed. Optionally, instead of + * freeing, the returned #JsonBuilder can be closed with + * #fb_json_bldr_close(). + * + * Returns: The new #JsonBuilder. + */ +JsonBuilder * +fb_json_bldr_new(JsonNodeType type); + +/** + * fb_json_bldr_close: + * @bldr: The #JsonBuilder. + * @type: The ending #JsonNodeType. + * @size: The return local for the size of the returned string. + * + * Closes the #JsonBuilder by returning a string representing the + * #JsonBuilder. The ending #JsonNodeType is likely to be + * #JSON_NODE_OBJECT. This calls #g_object_unref(). The returned + * string should be freed with #g_free() when no longer needed. + * + * Returns: The string representation of the #JsonBuilder. + */ +gchar * +fb_json_bldr_close(JsonBuilder *bldr, JsonNodeType type, gsize *size); + +/** + * fb_json_bldr_arr_begin: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * + * Begins an array member in the #JsonBuilder. + */ +void +fb_json_bldr_arr_begin(JsonBuilder *bldr, const gchar *name); + +/** + * fb_json_bldr_arr_end: + * @bldr: The #JsonBuilder. + * + * Ends an array member in the #JsonBuilder. + */ +void +fb_json_bldr_arr_end(JsonBuilder *bldr); + +/** + * fb_json_bldr_obj_begin: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * + * Begins an object member in the #JsonBuilder. + */ +void +fb_json_bldr_obj_begin(JsonBuilder *bldr, const gchar *name); + +/** + * fb_json_bldr_obj_end: + * @bldr: The #JsonBuilder. + * + * Ends an array member in the #JsonBuilder. + */ +void +fb_json_bldr_obj_end(JsonBuilder *bldr); + +/** + * fb_json_bldr_add_bool: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a boolean memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_bool(JsonBuilder *bldr, const gchar *name, gboolean value); + +/** + * fb_json_bldr_add_dbl: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a floating point memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_dbl(JsonBuilder *bldr, const gchar *name, gdouble value); + +/** + * fb_json_bldr_add_int: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a integer memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_int(JsonBuilder *bldr, const gchar *name, gint64 value); + +/** + * fb_json_bldr_add_str: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a string memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_str(JsonBuilder *bldr, const gchar *name, const gchar *value); + +/** + * fb_json_bldr_add_strf: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Adds a formatted string memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_strf(JsonBuilder *bldr, const gchar *name, + const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_json_node_new: + * @data: The string JSON. + * @size: The size of @json or -1 if null-terminated. + * @error: The return location for the #GError or #NULL. + * + * Creates a new #JsonNode. The returned #JsonBuilder should be freed + * wuth #json_node_free() when no longer needed. + * + * Returns: The new #JsonNode. + */ +JsonNode * +fb_json_node_new(const gchar *data, gssize size, GError **error); + +/** + * fb_json_node_get: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a new #JsonNode value from a parent #JsonNode with a #JsonPath + * expression. The returned #JsonNode should be freed with + * #json_node_free() when no longer needed. + * + * Returns: The new #JsonNode. + */ +JsonNode * +fb_json_node_get(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_nth: + * @root: The root #JsonNode. + * @n: The index number. + * + * Gets a #JsonNode value from a parent #JsonNode by index. The + * returned #JsonNode should not be freed. + * + * Return: The #JsonNode. + */ +JsonNode * +fb_json_node_get_nth(JsonNode *root, guint n); + +/** + * fb_json_node_get_arr: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a new #JsonArray value from a parent #JsonNode with a #JsonPath + * expression. The returned #JsonArray should be freed with + * #json_array_unref() when no longer needed. + * + * Returns: The new #JsonArray. + */ +JsonArray * +fb_json_node_get_arr(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_bool: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a boolean value from a parent #JsonNode with a #JsonPath + * expression. + * + * Returns: The boolean value. + */ +gboolean +fb_json_node_get_bool(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_dbl: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a floating point value from a parent #JsonNode with a #JsonPath + * expression. + * + * Returns: The floating point value. + */ +gdouble +fb_json_node_get_dbl(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_int: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets an integer value from a parent #JsonNode with a #JsonPath + * expression. + * + * Returns: The integer value. + */ +gint64 +fb_json_node_get_int(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_str: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets an string value from a parent #JsonNode with a #JsonPath + * expression. The returned string should be freed with #g_free() + * when no longer needed. + * + * Returns: The string value. + */ +gchar * +fb_json_node_get_str(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_values_new: + * @root: The root #JsonNode. + * + * Creates a new #FbJsonValues. The returned #FbJsonValues should be + * freed with #g_object_unref when no longer needed. + * + * Returns: The new #FbJsonValues. + */ +FbJsonValues * +fb_json_values_new(JsonNode *root); + +/** + * fb_json_values_add: + * @values: The #FbJsonValues. + * @type: The #FbJsonType. + * @required: #TRUE if the node is required, otherwise #FALSE. + * @expr: The #JsonPath expression. + * + * Adds a new #FbJsonValue to the #FbJsonValues. + */ +void +fb_json_values_add(FbJsonValues *values, FbJsonType type, gboolean required, + const gchar *expr); + +/** + * fb_json_values_get_root: + * @values: The #FbJsonValues. + * + * Gets the current working root #JsonNode. This is either the current + * array #JsonNode or the root #JsonNode. The returned #JsonNode should + * not be freed. + */ +JsonNode * +fb_json_values_get_root(FbJsonValues *values); + +/** + * fb_json_values_set_array: + * @values: The #FbJsonValues. + * @required: #TRUE if the node is required, otherwise #FALSE. + * @expr: The #JsonPath expression. + * + * Sets the #JsonPath for an array to base all #FbJsonValue's off. + */ +void +fb_json_values_set_array(FbJsonValues *values, gboolean required, + const gchar *expr); + +/** + * fb_json_values_update: + * @values: The #FbJsonValues. + * @error: The return location for the #GError or #NULL. + * + * Updates the current working root. This should be called after all of + * the #FbJsonValue's have been added with #fb_json_values_add(). If an + * array was set with #fb_json_values_set_array(), then this should be + * called in a while loop, until #FALSE is returned. + * + * Returns: #TRUE if the values were updated, otherwise #FALSE. + */ +gboolean +fb_json_values_update(FbJsonValues *values, GError **error); + +/** + * fb_json_values_next: + * @values: The #FbJsonValues. + * + * Gets the next #GValue from the #FbJsonValues. Before calling this + * function, #fb_json_values_update() must be called. + * + * Returns: The #GValue. + */ +const GValue * +fb_json_values_next(FbJsonValues *values); + +/** + * fb_json_values_next_bool: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next boolean value from the #FbJsonValues. Before calling + * this function, #fb_json_values_update() must be called. + * + * Returns: The boolean value. + */ +gboolean +fb_json_values_next_bool(FbJsonValues *values, gboolean defval); + +/** + * fb_json_values_next_dbl: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next floating point value from the #FbJsonValues. Before + * calling this function, #fb_json_values_update() must be called. + * + * Returns: The floating point value. + */ +gdouble +fb_json_values_next_dbl(FbJsonValues *values, gdouble defval); + +/** + * fb_json_values_next_int: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next integer value from the #FbJsonValues. Before calling + * this function, #fb_json_values_update() must be called. + * + * Returns: The integer value. + */ +gint64 +fb_json_values_next_int(FbJsonValues *values, gint64 defval); + +/** + * fb_json_values_next_str: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next string value from the #FbJsonValues. Before calling + * this function, #fb_json_values_update() must be called. + * + * Returns: The string value. + */ +const gchar * +fb_json_values_next_str(FbJsonValues *values, const gchar *defval); + +/** + * fb_json_values_next_str_dup: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next duplicate string value from the #FbJsonValues. Before + * calling this function, #fb_json_values_update() must be called. + * + * Returns: The duplicate string value. + */ +gchar * +fb_json_values_next_str_dup(FbJsonValues *values, const gchar *defval); + +#endif /* _FACEBOOK_JSON_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/meson.build b/pidgin/libpurple/protocols/facebook/meson.build new file mode 100644 index 0000000..5de81ec --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/meson.build @@ -0,0 +1,35 @@ +FACEBOOKSOURCES = [ + 'api.c', + 'api.h', + 'data.c', + 'data.h', + 'facebook.h', + 'facebook.c', + 'http.c', + 'http.h', + 'id.h', + 'json.c', + 'json.h', + 'mqtt.c', + 'mqtt.h', + 'thrift.c', + 'thrift.h', + 'util.c', + 'util.h' +] + +if STATIC_FACEBOOK + facebook_prpl = static_library('facebook', FACEBOOKSOURCES, + c_args : '-DPURPLE_STATIC_PRPL', + dependencies : [json, libpurple_dep, glib]) +elif DYNAMIC_FACEBOOK + facebook_prpl = shared_library('facebook', FACEBOOKSOURCES, + dependencies : [json, libpurple_dep, glib], + install : true, install_dir : PURPLE_PLUGINDIR) +endif + +# Used to produce docs. +facebook_inc = include_directories('.') +facebook_dep = declare_dependency( + link_with : facebook_prpl, + dependencies : [json, libpurple_dep, glib]) diff --git a/pidgin/libpurple/protocols/facebook/mqtt.c b/pidgin/libpurple/protocols/facebook/mqtt.c new file mode 100644 index 0000000..da279cb --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/mqtt.c @@ -0,0 +1,1063 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include <glib/gprintf.h> +#include <stdarg.h> +#include <string.h> + +#include "account.h" +#include "eventloop.h" +#include "glibcompat.h" +#include "purple-gio.h" +#include "queuedoutputstream.h" + +#include "mqtt.h" +#include "util.h" + +struct _FbMqttPrivate +{ + PurpleConnection *gc; + GIOStream *conn; + GBufferedInputStream *input; + PurpleQueuedOutputStream *output; + GCancellable *cancellable; + gboolean connected; + guint16 mid; + + GByteArray *rbuf; + gsize remz; + + gint tev; +}; + +struct _FbMqttMessagePrivate +{ + FbMqttMessageType type; + FbMqttMessageFlags flags; + + GByteArray *bytes; + guint offset; + guint pos; + + gboolean local; +}; + +G_DEFINE_TYPE(FbMqtt, fb_mqtt, G_TYPE_OBJECT); +G_DEFINE_TYPE(FbMqttMessage, fb_mqtt_message, G_TYPE_OBJECT); + +static void fb_mqtt_read_packet(FbMqtt *mqtt); + +static void +fb_mqtt_dispose(GObject *obj) +{ + FbMqtt *mqtt = FB_MQTT(obj); + FbMqttPrivate *priv = mqtt->priv; + + fb_mqtt_close(mqtt); + g_byte_array_free(priv->rbuf, TRUE); +} + +static void +fb_mqtt_class_init(FbMqttClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_mqtt_dispose; + g_type_class_add_private(klass, sizeof (FbMqttPrivate)); + + /** + * FbMqtt::connect: + * @mqtt: The #FbMqtt. + * + * Emitted upon the successful completion of the connection + * process. This is emitted as a result of #fb_mqtt_connect(). + */ + g_signal_new("connect", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * FbMqtt::error: + * @mqtt: The #FbMqtt. + * @error: The #GError. + * + * Emitted whenever an error is hit within the #FbMqtt. This + * should close the #FbMqtt with #fb_mqtt_close(). + */ + g_signal_new("error", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_ERROR); + + /** + * FbMqtt::open: + * @mqtt: The #FbMqtt. + * + * Emitted upon the successful opening of the remote socket. + * This is emitted as a result of #fb_mqtt_open(). This should + * call #fb_mqtt_connect(). + */ + g_signal_new("open", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * FbMqtt::publish: + * @mqtt: The #FbMqtt. + * @topic: The topic. + * @pload: The payload. + * + * Emitted upon an incoming message from the steam. + */ + g_signal_new("publish", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, G_TYPE_STRING, G_TYPE_BYTE_ARRAY); +} + +static void +fb_mqtt_init(FbMqtt *mqtt) +{ + FbMqttPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(mqtt, FB_TYPE_MQTT, FbMqttPrivate); + mqtt->priv = priv; + + priv->rbuf = g_byte_array_new(); +} + +static void +fb_mqtt_message_dispose(GObject *obj) +{ + FbMqttMessagePrivate *priv = FB_MQTT_MESSAGE(obj)->priv; + + if ((priv->bytes != NULL) && priv->local) { + g_byte_array_free(priv->bytes, TRUE); + } +} + +static void +fb_mqtt_message_class_init(FbMqttMessageClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_mqtt_message_dispose; + g_type_class_add_private(klass, sizeof (FbMqttMessagePrivate)); +} + +static void +fb_mqtt_message_init(FbMqttMessage *msg) +{ + FbMqttMessagePrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(msg, FB_TYPE_MQTT_MESSAGE, + FbMqttMessagePrivate); + msg->priv = priv; +} + +GQuark +fb_mqtt_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-mqtt-error-quark"); + } + + return q; +} + +FbMqtt * +fb_mqtt_new(PurpleConnection *gc) +{ + FbMqtt *mqtt; + FbMqttPrivate *priv; + + g_return_val_if_fail(PURPLE_IS_CONNECTION(gc), NULL); + + mqtt = g_object_new(FB_TYPE_MQTT, NULL); + priv = mqtt->priv; + priv->gc = gc; + + return mqtt; +}; + +void +fb_mqtt_close(FbMqtt *mqtt) +{ + FbMqttPrivate *priv; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + priv = mqtt->priv; + + if (priv->tev > 0) { + g_source_remove(priv->tev); + priv->tev = 0; + } + + if (priv->cancellable != NULL) { + g_cancellable_cancel(priv->cancellable); + g_clear_object(&priv->cancellable); + } + + if (priv->conn != NULL) { + purple_gio_graceful_close(priv->conn, + G_INPUT_STREAM(priv->input), + G_OUTPUT_STREAM(priv->output)); + g_clear_object(&priv->input); + g_clear_object(&priv->output); + g_clear_object(&priv->conn); + } + + priv->connected = FALSE; + g_byte_array_set_size(priv->rbuf, 0); +} + +static void +fb_mqtt_take_error(FbMqtt *mqtt, GError *err, const gchar *prefix) +{ + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* Return as cancelled means the connection is closing */ + g_error_free(err); + return; + } + + /* Now we can check for programming errors */ + g_return_if_fail(FB_IS_MQTT(mqtt)); + + if (prefix != NULL) { + g_prefix_error(&err, "%s: ", prefix); + } + + g_signal_emit_by_name(mqtt, "error", err); + g_error_free(err); +} + +void +fb_mqtt_error(FbMqtt *mqtt, FbMqttError error, const gchar *format, ...) +{ + GError *err; + va_list ap; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + + va_start(ap, format); + err = g_error_new_valist(FB_MQTT_ERROR, error, format, ap); + va_end(ap); + + g_signal_emit_by_name(mqtt, "error", err); + g_error_free(err); +} + +static gboolean +fb_mqtt_cb_timeout(gpointer data) +{ + FbMqtt *mqtt = data; + FbMqttPrivate *priv = mqtt->priv; + + priv->tev = 0; + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, _("Connection timed out")); + return FALSE; +} + +static void +fb_mqtt_timeout_clear(FbMqtt *mqtt) +{ + FbMqttPrivate *priv = mqtt->priv; + + if (priv->tev > 0) { + g_source_remove(priv->tev); + priv->tev = 0; + } +} + +static void +fb_mqtt_timeout(FbMqtt *mqtt) +{ + FbMqttPrivate *priv = mqtt->priv; + + fb_mqtt_timeout_clear(mqtt); + priv->tev = g_timeout_add(FB_MQTT_TIMEOUT_CONN, + fb_mqtt_cb_timeout, mqtt); +} + +static gboolean +fb_mqtt_cb_ping(gpointer data) +{ + FbMqtt *mqtt = data; + FbMqttMessage *msg; + FbMqttPrivate *priv = mqtt->priv; + + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_PINGREQ, 0); + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); + + priv->tev = 0; + fb_mqtt_timeout(mqtt); + return FALSE; +} + +static void +fb_mqtt_ping(FbMqtt *mqtt) +{ + FbMqttPrivate *priv = mqtt->priv; + + fb_mqtt_timeout_clear(mqtt); + priv->tev = g_timeout_add(FB_MQTT_TIMEOUT_PING, + fb_mqtt_cb_ping, mqtt); +} + +static void +fb_mqtt_cb_fill(GObject *source, GAsyncResult *res, gpointer data) +{ + GBufferedInputStream *input = G_BUFFERED_INPUT_STREAM(source); + FbMqtt *mqtt = data; + gssize ret; + GError *err = NULL; + + ret = g_buffered_input_stream_fill_finish(input, res, &err); + + if (ret < 1) { + if (ret == 0) { + err = g_error_new_literal(G_IO_ERROR, + G_IO_ERROR_CONNECTION_CLOSED, + _("Connection closed")); + } + + fb_mqtt_take_error(mqtt, err, _("Failed to read fixed header")); + return; + } + + fb_mqtt_read_packet(mqtt); +} + +static void +fb_mqtt_cb_read_packet(GObject *source, GAsyncResult *res, gpointer data) +{ + FbMqtt *mqtt = data; + FbMqttPrivate *priv; + gssize ret; + FbMqttMessage *msg; + GError *err = NULL; + + ret = g_input_stream_read_finish(G_INPUT_STREAM(source), res, &err); + + if (ret < 1) { + if (ret == 0) { + err = g_error_new_literal(G_IO_ERROR, + G_IO_ERROR_CONNECTION_CLOSED, + _("Connection closed")); + } + + fb_mqtt_take_error(mqtt, err, _("Failed to read packet data")); + return; + } + + priv = mqtt->priv; + priv->remz -= ret; + + if (priv->remz > 0) { + g_input_stream_read_async(G_INPUT_STREAM(source), + priv->rbuf->data + + priv->rbuf->len - priv->remz, priv->remz, + G_PRIORITY_DEFAULT, priv->cancellable, + fb_mqtt_cb_read_packet, mqtt); + return; + } + + msg = fb_mqtt_message_new_bytes(priv->rbuf); + + if (G_UNLIKELY(msg == NULL)) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to parse message")); + return; + } + + fb_mqtt_read(mqtt, msg); + g_object_unref(msg); + + /* Read another packet if connection wasn't reset in fb_mqtt_read() */ + if (fb_mqtt_connected(mqtt, FALSE)) { + fb_mqtt_read_packet(mqtt); + } +} + +static void +fb_mqtt_read_packet(FbMqtt *mqtt) +{ + FbMqttPrivate *priv = mqtt->priv; + const guint8 const *buf; + gsize count = 0; + gsize pos; + guint mult = 1; + guint8 byte; + gsize size = 0; + + buf = g_buffered_input_stream_peek_buffer(priv->input, &count); + + /* Start at 1 to skip the first byte */ + pos = 1; + + do { + if (pos >= count) { + /* Not enough data yet, try again later */ + g_buffered_input_stream_fill_async(priv->input, -1, + G_PRIORITY_DEFAULT, priv->cancellable, + fb_mqtt_cb_fill, mqtt); + return; + } + + byte = *(buf + pos++); + + size += (byte & 127) * mult; + mult *= 128; + } while ((byte & 128) != 0); + + /* Add header to size */ + size += pos; + + g_byte_array_set_size(priv->rbuf, size); + priv->remz = size; + + /* TODO: Use g_input_stream_read_all_async() when available. */ + /* TODO: Alternately, it would be nice to let the + * FbMqttMessage directly use the GBufferedInputStream + * buffer instead of copying it, provided it's consumed + * before the next read. + */ + g_input_stream_read_async(G_INPUT_STREAM(priv->input), + priv->rbuf->data, priv->rbuf->len, + G_PRIORITY_DEFAULT, priv->cancellable, + fb_mqtt_cb_read_packet, mqtt); +} + +void +fb_mqtt_read(FbMqtt *mqtt, FbMqttMessage *msg) +{ + FbMqttMessage *nsg; + FbMqttPrivate *priv; + FbMqttMessagePrivate *mriv; + GByteArray *wytes; + gchar *str; + guint8 chr; + guint16 mid; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = mqtt->priv; + mriv = msg->priv; + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, mriv->bytes, + "Reading %d (flags: 0x%0X)", + mriv->type, mriv->flags); + + switch (mriv->type) { + case FB_MQTT_MESSAGE_TYPE_CONNACK: + if (!fb_mqtt_message_read_byte(msg, NULL) || + !fb_mqtt_message_read_byte(msg, &chr)) + { + break; + } + + if (chr != FB_MQTT_ERROR_SUCCESS) { + fb_mqtt_error(mqtt, chr, _("Connection failed (%u)"), + chr); + return; + } + + priv->connected = TRUE; + fb_mqtt_ping(mqtt); + g_signal_emit_by_name(mqtt, "connect"); + return; + + case FB_MQTT_MESSAGE_TYPE_PUBLISH: + if (!fb_mqtt_message_read_str(msg, &str)) { + break; + } + + if ((mriv->flags & FB_MQTT_MESSAGE_FLAG_QOS1) || + (mriv->flags & FB_MQTT_MESSAGE_FLAG_QOS2)) + { + if (mriv->flags & FB_MQTT_MESSAGE_FLAG_QOS1) { + chr = FB_MQTT_MESSAGE_TYPE_PUBACK; + } else { + chr = FB_MQTT_MESSAGE_TYPE_PUBREC; + } + + if (!fb_mqtt_message_read_mid(msg, &mid)) { + g_free(str); + break; + } + + nsg = fb_mqtt_message_new(chr, 0); + fb_mqtt_message_write_u16(nsg, mid); + fb_mqtt_write(mqtt, nsg); + g_object_unref(nsg); + } + + wytes = g_byte_array_new(); + fb_mqtt_message_read_r(msg, wytes); + g_signal_emit_by_name(mqtt, "publish", str, wytes); + g_byte_array_free(wytes, TRUE); + g_free(str); + return; + + case FB_MQTT_MESSAGE_TYPE_PUBREL: + if (!fb_mqtt_message_read_mid(msg, &mid)) { + break; + } + + nsg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_PUBCOMP, 0); + fb_mqtt_message_write_u16(nsg, mid); /* Message identifier */ + fb_mqtt_write(mqtt, nsg); + g_object_unref(nsg); + return; + + case FB_MQTT_MESSAGE_TYPE_PINGRESP: + fb_mqtt_ping(mqtt); + return; + + case FB_MQTT_MESSAGE_TYPE_PUBACK: + case FB_MQTT_MESSAGE_TYPE_PUBCOMP: + case FB_MQTT_MESSAGE_TYPE_SUBACK: + case FB_MQTT_MESSAGE_TYPE_UNSUBACK: + return; + + default: + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Unknown packet (%u)"), mriv->type); + return; + } + + /* Since no case returned, there was a parse error. */ + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to parse message")); +} + +static void +fb_mqtt_cb_flush(GObject *source, GAsyncResult *res, gpointer data) +{ + FbMqtt *mqtt = data; + GError *err = NULL; + + if (!g_output_stream_flush_finish(G_OUTPUT_STREAM(source), + res, &err)) { + fb_mqtt_take_error(mqtt, err, _("Failed to write data")); + return; + } +} + +void +fb_mqtt_write(FbMqtt *mqtt, FbMqttMessage *msg) +{ + const GByteArray *bytes; + FbMqttMessagePrivate *mriv; + FbMqttPrivate *priv; + GBytes *gbytes; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = mqtt->priv; + mriv = msg->priv; + + bytes = fb_mqtt_message_bytes(msg); + + if (G_UNLIKELY(bytes == NULL)) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to format data")); + return; + } + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, mriv->bytes, + "Writing %d (flags: 0x%0X)", + mriv->type, mriv->flags); + + /* TODO: Would be nice to refactor this to not require copying bytes */ + gbytes = g_bytes_new(bytes->data, bytes->len); + purple_queued_output_stream_push_bytes(priv->output, gbytes); + g_bytes_unref(gbytes); + + if (!g_output_stream_has_pending(G_OUTPUT_STREAM(priv->output))) { + g_output_stream_flush_async(G_OUTPUT_STREAM(priv->output), + G_PRIORITY_DEFAULT, priv->cancellable, + fb_mqtt_cb_flush, mqtt); + } +} + +static void +fb_mqtt_cb_open(GObject *source, GAsyncResult *res, gpointer data) +{ + FbMqtt *mqtt = data; + FbMqttPrivate *priv; + GSocketConnection *conn; + GError *err = NULL; + + conn = g_socket_client_connect_to_host_finish(G_SOCKET_CLIENT(source), + res, &err); + + if (conn == NULL) { + fb_mqtt_take_error(mqtt, err, NULL); + return; + } + + fb_mqtt_timeout_clear(mqtt); + + priv = mqtt->priv; + priv->conn = G_IO_STREAM(conn); + priv->input = G_BUFFERED_INPUT_STREAM(g_buffered_input_stream_new( + g_io_stream_get_input_stream(priv->conn))); + priv->output = purple_queued_output_stream_new( + g_io_stream_get_output_stream(priv->conn)); + + fb_mqtt_read_packet(mqtt); + + g_signal_emit_by_name(mqtt, "open"); +} + +void +fb_mqtt_open(FbMqtt *mqtt, const gchar *host, gint port) +{ + FbMqttPrivate *priv; + PurpleAccount *acc; + GSocketClient *client; + GError *err = NULL; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + priv = mqtt->priv; + + acc = purple_connection_get_account(priv->gc); + fb_mqtt_close(mqtt); + + client = purple_gio_socket_client_new(acc, &err); + + if (client == NULL) { + fb_mqtt_take_error(mqtt, err, NULL); + return; + } + + priv->cancellable = g_cancellable_new(); + + g_socket_client_set_tls(client, TRUE); + g_socket_client_connect_to_host_async(client, host, port, + priv->cancellable, fb_mqtt_cb_open, mqtt); + g_object_unref(client); + + fb_mqtt_timeout(mqtt); +} + +void +fb_mqtt_connect(FbMqtt *mqtt, guint8 flags, const GByteArray *pload) +{ + FbMqttMessage *msg; + + g_return_if_fail(!fb_mqtt_connected(mqtt, FALSE)); + g_return_if_fail(pload != NULL); + + /* Facebook always sends a CONNACK, use QoS1 */ + flags |= FB_MQTT_CONNECT_FLAG_QOS1; + + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_CONNECT, 0); + fb_mqtt_message_write_str(msg, FB_MQTT_NAME); /* Protocol name */ + fb_mqtt_message_write_byte(msg, FB_MQTT_LEVEL); /* Protocol level */ + fb_mqtt_message_write_byte(msg, flags); /* Flags */ + fb_mqtt_message_write_u16(msg, FB_MQTT_KA); /* Keep alive */ + + fb_mqtt_message_write(msg, pload->data, pload->len); + fb_mqtt_write(mqtt, msg); + + fb_mqtt_timeout(mqtt); + g_object_unref(msg); +} + +gboolean +fb_mqtt_connected(FbMqtt *mqtt, gboolean error) +{ + FbMqttPrivate *priv; + gboolean connected; + + g_return_val_if_fail(FB_IS_MQTT(mqtt), FALSE); + priv = mqtt->priv; + connected = (priv->conn != NULL) && priv->connected; + + if (!connected && error) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Not connected")); + } + + return connected; +} + +void +fb_mqtt_disconnect(FbMqtt *mqtt) +{ + FbMqttMessage *msg; + + if (G_UNLIKELY(!fb_mqtt_connected(mqtt, FALSE))) { + return; + } + + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_DISCONNECT, 0); + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); + fb_mqtt_close(mqtt); +} + +void +fb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, const GByteArray *pload) +{ + FbMqttMessage *msg; + FbMqttPrivate *priv; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(fb_mqtt_connected(mqtt, FALSE)); + priv = mqtt->priv; + + /* Message identifier not required, but for consistency use QoS1 */ + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_PUBLISH, + FB_MQTT_MESSAGE_FLAG_QOS1); + + fb_mqtt_message_write_str(msg, topic); /* Message topic */ + fb_mqtt_message_write_mid(msg, &priv->mid); /* Message identifier */ + + if (pload != NULL) { + fb_mqtt_message_write(msg, pload->data, pload->len); + } + + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); +} + +void +fb_mqtt_subscribe(FbMqtt *mqtt, const gchar *topic1, guint16 qos1, ...) +{ + const gchar *topic; + FbMqttMessage *msg; + FbMqttPrivate *priv; + guint16 qos; + va_list ap; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(fb_mqtt_connected(mqtt, FALSE)); + priv = mqtt->priv; + + /* Facebook requires a message identifier, use QoS1 */ + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_SUBSCRIBE, + FB_MQTT_MESSAGE_FLAG_QOS1); + + fb_mqtt_message_write_mid(msg, &priv->mid); /* Message identifier */ + fb_mqtt_message_write_str(msg, topic1); /* First topics */ + fb_mqtt_message_write_byte(msg, qos1); /* First QoS value */ + + va_start(ap, qos1); + + while ((topic = va_arg(ap, const gchar*)) != NULL) { + qos = va_arg(ap, guint); + fb_mqtt_message_write_str(msg, topic); /* Remaining topics */ + fb_mqtt_message_write_byte(msg, qos); /* Remaining QoS values */ + } + + va_end(ap); + + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); +} + +void +fb_mqtt_unsubscribe(FbMqtt *mqtt, const gchar *topic1, ...) +{ + const gchar *topic; + FbMqttMessage *msg; + FbMqttPrivate *priv; + va_list ap; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(fb_mqtt_connected(mqtt, FALSE)); + priv = mqtt->priv; + + /* Facebook requires a message identifier, use QoS1 */ + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE, + FB_MQTT_MESSAGE_FLAG_QOS1); + + fb_mqtt_message_write_mid(msg, &priv->mid); /* Message identifier */ + fb_mqtt_message_write_str(msg, topic1); /* First topic */ + + va_start(ap, topic1); + + while ((topic = va_arg(ap, const gchar*)) != NULL) { + fb_mqtt_message_write_str(msg, topic); /* Remaining topics */ + } + + va_end(ap); + + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); +} + +FbMqttMessage * +fb_mqtt_message_new(FbMqttMessageType type, FbMqttMessageFlags flags) +{ + FbMqttMessage *msg; + FbMqttMessagePrivate *priv; + + msg = g_object_new(FB_TYPE_MQTT_MESSAGE, NULL); + priv = msg->priv; + + priv->type = type; + priv->flags = flags; + priv->bytes = g_byte_array_new(); + priv->local = TRUE; + + return msg; +} + +FbMqttMessage * +fb_mqtt_message_new_bytes(GByteArray *bytes) +{ + FbMqttMessage *msg; + FbMqttMessagePrivate *priv; + guint8 *byte; + + g_return_val_if_fail(bytes != NULL, NULL); + g_return_val_if_fail(bytes->len >= 2, NULL); + + msg = g_object_new(FB_TYPE_MQTT_MESSAGE, NULL); + priv = msg->priv; + + priv->bytes = bytes; + priv->local = FALSE; + priv->type = (*bytes->data & 0xF0) >> 4; + priv->flags = *bytes->data & 0x0F; + + /* Skip the fixed header */ + for (byte = priv->bytes->data + 1; (*(byte++) & 128) != 0; ); + priv->offset = byte - bytes->data; + priv->pos = priv->offset; + + return msg; +} + +void +fb_mqtt_message_reset(FbMqttMessage *msg) +{ + FbMqttMessagePrivate *priv; + + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = msg->priv; + + if (priv->offset > 0) { + g_byte_array_remove_range(priv->bytes, 0, priv->offset); + priv->offset = 0; + priv->pos = 0; + } +} + +const GByteArray * +fb_mqtt_message_bytes(FbMqttMessage *msg) +{ + FbMqttMessagePrivate *priv; + guint i; + guint8 byte; + guint8 sbuf[4]; + guint32 size; + + g_return_val_if_fail(FB_IS_MQTT_MESSAGE(msg), NULL); + priv = msg->priv; + + i = 0; + size = priv->bytes->len - priv->offset; + + do { + if (G_UNLIKELY(i >= G_N_ELEMENTS(sbuf))) { + return NULL; + } + + byte = size % 128; + size /= 128; + + if (size > 0) { + byte |= 128; + } + + sbuf[i++] = byte; + } while (size > 0); + + fb_mqtt_message_reset(msg); + g_byte_array_prepend(priv->bytes, sbuf, i); + + byte = ((priv->type & 0x0F) << 4) | (priv->flags & 0x0F); + g_byte_array_prepend(priv->bytes, &byte, sizeof byte); + + priv->pos = (i + 1) * (sizeof byte); + return priv->bytes; +} + +gboolean +fb_mqtt_message_read(FbMqttMessage *msg, gpointer data, guint size) +{ + FbMqttMessagePrivate *priv; + + g_return_val_if_fail(FB_IS_MQTT_MESSAGE(msg), FALSE); + priv = msg->priv; + + if ((priv->pos + size) > priv->bytes->len) { + return FALSE; + } + + if ((data != NULL) && (size > 0)) { + memcpy(data, priv->bytes->data + priv->pos, size); + } + + priv->pos += size; + return TRUE; +} + +gboolean +fb_mqtt_message_read_r(FbMqttMessage *msg, GByteArray *bytes) +{ + FbMqttMessagePrivate *priv; + guint size; + + g_return_val_if_fail(FB_IS_MQTT_MESSAGE(msg), FALSE); + priv = msg->priv; + size = priv->bytes->len - priv->pos; + + if (G_LIKELY(size > 0)) { + g_byte_array_append(bytes, priv->bytes->data + priv->pos, + size); + } + + return TRUE; +} + +gboolean +fb_mqtt_message_read_byte(FbMqttMessage *msg, guint8 *value) +{ + return fb_mqtt_message_read(msg, value, sizeof *value); +} + +gboolean +fb_mqtt_message_read_mid(FbMqttMessage *msg, guint16 *value) +{ + return fb_mqtt_message_read_u16(msg, value); +} + +gboolean +fb_mqtt_message_read_u16(FbMqttMessage *msg, guint16 *value) +{ + if (!fb_mqtt_message_read(msg, value, sizeof *value)) { + return FALSE; + } + + if (value != NULL) { + *value = g_ntohs(*value); + } + + return TRUE; +} + +gboolean +fb_mqtt_message_read_str(FbMqttMessage *msg, gchar **value) +{ + guint8 *data; + guint16 size; + + if (!fb_mqtt_message_read_u16(msg, &size)) { + return FALSE; + } + + if (value != NULL) { + data = g_new(guint8, size + 1); + data[size] = 0; + } else { + data = NULL; + } + + if (!fb_mqtt_message_read(msg, data, size)) { + g_free(data); + return FALSE; + } + + if (value != NULL) { + *value = (gchar *) data; + } + + return TRUE; +} + +void +fb_mqtt_message_write(FbMqttMessage *msg, gconstpointer data, guint size) +{ + FbMqttMessagePrivate *priv; + + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = msg->priv; + + g_byte_array_append(priv->bytes, data, size); + priv->pos += size; +} + +void +fb_mqtt_message_write_byte(FbMqttMessage *msg, guint8 value) +{ + fb_mqtt_message_write(msg, &value, sizeof value); +} + +void +fb_mqtt_message_write_mid(FbMqttMessage *msg, guint16 *value) +{ + g_return_if_fail(value != NULL); + fb_mqtt_message_write_u16(msg, ++(*value)); +} + +void +fb_mqtt_message_write_u16(FbMqttMessage *msg, guint16 value) +{ + value = g_htons(value); + fb_mqtt_message_write(msg, &value, sizeof value); +} + +void +fb_mqtt_message_write_str(FbMqttMessage *msg, const gchar *value) +{ + gint16 size; + + g_return_if_fail(value != NULL); + + size = strlen(value); + fb_mqtt_message_write_u16(msg, size); + fb_mqtt_message_write(msg, value, size); +} diff --git a/pidgin/libpurple/protocols/facebook/mqtt.h b/pidgin/libpurple/protocols/facebook/mqtt.h new file mode 100644 index 0000000..30172eb --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/mqtt.h @@ -0,0 +1,609 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_MQTT_H_ +#define _FACEBOOK_MQTT_H_ + +/** + * SECTION:mqtt + * @section_id: facebook-mqtt + * @short_description: <filename>mqtt.h</filename> + * @title: MQTT Connection + * + * The MQTT connection. + */ + +#include <glib.h> +#include <string.h> + +#include "connection.h" + +#define FB_TYPE_MQTT (fb_mqtt_get_type()) +#define FB_MQTT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_MQTT, FbMqtt)) +#define FB_MQTT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_MQTT, FbMqttClass)) +#define FB_IS_MQTT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_MQTT)) +#define FB_IS_MQTT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_MQTT)) +#define FB_MQTT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_MQTT, FbMqttClass)) + +#define FB_TYPE_MQTT_MESSAGE (fb_mqtt_message_get_type()) +#define FB_MQTT_MESSAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_MQTT_MESSAGE, FbMqttMessage)) +#define FB_MQTT_MESSAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_MQTT_MESSAGE, FbMqttMessageClass)) +#define FB_IS_MQTT_MESSAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_MQTT_MESSAGE)) +#define FB_IS_MQTT_MESSAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_MQTT_MESSAGE)) +#define FB_MQTT_MESSAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_MQTT_MESSAGE, FbMqttMessageClass)) + +/** + * FB_MQTT_NAME: + * + * The name of the MQTT version. + */ +#define FB_MQTT_NAME "MQTToT" + +/** + * FB_MQTT_LEVEL: + * + * The level of the MQTT version. + */ +#define FB_MQTT_LEVEL 3 + +/** + * FB_MQTT_KA: + * + * The keep-alive timeout, in seconds, of the MQTT connection. + */ +#define FB_MQTT_KA 60 + +/** + * FB_MQTT_HOST: + * + * The MQTT host name for Facebook. + */ +#define FB_MQTT_HOST "mqtt.facebook.com" + +/** + * FB_MQTT_PORT: + * + * The MQTT host port for Facebook. + */ +#define FB_MQTT_PORT 443 + +/** + * FB_MQTT_TIMEOUT_CONN: + * + * The timeout, in milliseconds, to wait for a PING back from the + * server. + */ +#define FB_MQTT_TIMEOUT_CONN (FB_MQTT_KA * 1500) + +/** + * FB_MQTT_TIMEOUT_PING: + * + * The timeout, in milliseconds, to send a PING to the server. + */ +#define FB_MQTT_TIMEOUT_PING (FB_MQTT_KA * 1000) + +/** + * FB_MQTT_ERROR: + * + * The #GQuark of the domain of MQTT errors. + */ +#define FB_MQTT_ERROR fb_mqtt_error_quark() + +typedef struct _FbMqtt FbMqtt; +typedef struct _FbMqttClass FbMqttClass; +typedef struct _FbMqttPrivate FbMqttPrivate; +typedef struct _FbMqttMessage FbMqttMessage; +typedef struct _FbMqttMessageClass FbMqttMessageClass; +typedef struct _FbMqttMessagePrivate FbMqttMessagePrivate; + +/** + * FbMqttConnectFlags: + * @FB_MQTT_CONNECT_FLAG_CLR: Clear the session. + * @FB_MQTT_CONNECT_FLAG_WILL: A will message is in the payload. + * @FB_MQTT_CONNECT_FLAG_RET: Retain the will message. + * @FB_MQTT_CONNECT_FLAG_PASS: A password is in the payload. + * @FB_MQTT_CONNECT_FLAG_USER: A user name is in the payload. + * @FB_MQTT_CONNECT_FLAG_QOS0: Use no quality of service. + * @FB_MQTT_CONNECT_FLAG_QOS1: Use level one quality of service. + * @FB_MQTT_CONNECT_FLAG_QOS2: Use level two quality of service. + * + * The #FbMqttMessage flags for the CONNECT message. + */ +typedef enum +{ + FB_MQTT_CONNECT_FLAG_CLR = 1 << 1, + FB_MQTT_CONNECT_FLAG_WILL = 1 << 2, + FB_MQTT_CONNECT_FLAG_RET = 1 << 5, + FB_MQTT_CONNECT_FLAG_PASS = 1 << 6, + FB_MQTT_CONNECT_FLAG_USER = 1 << 7, + FB_MQTT_CONNECT_FLAG_QOS0 = 0 << 3, + FB_MQTT_CONNECT_FLAG_QOS1 = 1 << 3, + FB_MQTT_CONNECT_FLAG_QOS2 = 2 << 3 +} FbMqttConnectFlags; + +/** + * FbMqttError: + * @FB_MQTT_ERROR_SUCCESS: There is no error. + * @FB_MQTT_ERROR_PRTVERS: Unacceptable protocol version. + * @FB_MQTT_ERROR_IDREJECT: Identifier rejected. + * @FB_MQTT_ERROR_SRVGONE: Server unavailable. + * @FB_MQTT_ERROR_USERPASS: Bad user name or password. + * @FB_MQTT_ERROR_UNAUTHORIZED: Not authorized. + * @FB_MQTT_ERROR_GENERAL: General failure. + * + * The error codes for the #FB_MQTT_ERROR domain. + */ +typedef enum +{ + FB_MQTT_ERROR_SUCCESS = 0, + FB_MQTT_ERROR_PRTVERS = 1, + FB_MQTT_ERROR_IDREJECT = 2, + FB_MQTT_ERROR_SRVGONE = 3, + FB_MQTT_ERROR_USERPASS = 4, + FB_MQTT_ERROR_UNAUTHORIZED = 5, + FB_MQTT_ERROR_GENERAL +} FbMqttError; + +/** + * FbMqttMessageFlags: + * @FB_MQTT_MESSAGE_FLAG_RET: Retain messages. + * @FB_MQTT_MESSAGE_FLAG_DUP: Duplicate delivery of control packet. + * @FB_MQTT_MESSAGE_FLAG_QOS0: Use no quality of service. + * @FB_MQTT_MESSAGE_FLAG_QOS1: Use level one quality of service. + * @FB_MQTT_MESSAGE_FLAG_QOS2: Use level two quality of service. + * + * The #FbMqttMessage flags. + */ +typedef enum +{ + FB_MQTT_MESSAGE_FLAG_RET = 1 << 0, + FB_MQTT_MESSAGE_FLAG_DUP = 1 << 3, + FB_MQTT_MESSAGE_FLAG_QOS0 = 0 << 1, + FB_MQTT_MESSAGE_FLAG_QOS1 = 1 << 1, + FB_MQTT_MESSAGE_FLAG_QOS2 = 2 << 1 +} FbMqttMessageFlags; + +/** + * FbMqttMessageType: + * @FB_MQTT_MESSAGE_TYPE_CONNECT: Requests a connection. + * @FB_MQTT_MESSAGE_TYPE_CONNACK: Connection acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_PUBLISH: Requests a message publication. + * @FB_MQTT_MESSAGE_TYPE_PUBACK: Publication acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_PUBREC: Publication received. + * @FB_MQTT_MESSAGE_TYPE_PUBREL: Publication released. + * @FB_MQTT_MESSAGE_TYPE_PUBCOMP: Publication complete. + * @FB_MQTT_MESSAGE_TYPE_SUBSCRIBE: Requests a subscription. + * @FB_MQTT_MESSAGE_TYPE_SUBACK: Subscription acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE: Requests an unsubscription. + * @FB_MQTT_MESSAGE_TYPE_UNSUBACK: Unsubscription acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_PINGREQ: Requests a ping response. + * @FB_MQTT_MESSAGE_TYPE_PINGRESP: Ping response. + * @FB_MQTT_MESSAGE_TYPE_DISCONNECT: Requests a disconnection. + * + * The #FbMqttMessage types. + */ +typedef enum +{ + FB_MQTT_MESSAGE_TYPE_CONNECT = 1, + FB_MQTT_MESSAGE_TYPE_CONNACK = 2, + FB_MQTT_MESSAGE_TYPE_PUBLISH = 3, + FB_MQTT_MESSAGE_TYPE_PUBACK = 4, + FB_MQTT_MESSAGE_TYPE_PUBREC = 5, + FB_MQTT_MESSAGE_TYPE_PUBREL = 6, + FB_MQTT_MESSAGE_TYPE_PUBCOMP = 7, + FB_MQTT_MESSAGE_TYPE_SUBSCRIBE = 8, + FB_MQTT_MESSAGE_TYPE_SUBACK = 9, + FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE = 10, + FB_MQTT_MESSAGE_TYPE_UNSUBACK = 11, + FB_MQTT_MESSAGE_TYPE_PINGREQ = 12, + FB_MQTT_MESSAGE_TYPE_PINGRESP = 13, + FB_MQTT_MESSAGE_TYPE_DISCONNECT = 14 +} FbMqttMessageType; + +/** + * FbMqtt: + * + * Represents an MQTT connection. + */ +struct _FbMqtt +{ + /*< private >*/ + GObject parent; + FbMqttPrivate *priv; +}; + +/** + * FbMqttClass: + * + * The base class for all #FbMqtt's. + */ +struct _FbMqttClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * FbMqttMessage: + * + * Represents a reader/writer for an MQTT message. + */ +struct _FbMqttMessage +{ + /*< private >*/ + GObject parent; + FbMqttMessagePrivate *priv; +}; + +/** + * FbMqttMessageClass: + * + * The base class for all #FbMqttMessageClass's. + */ +struct _FbMqttMessageClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_mqtt_get_type: + * + * Returns: The #GType for an #FbMqtt. + */ +GType +fb_mqtt_get_type(void); + +/** + * fb_mqtt_message_get_type: + * + * Returns: The #GType for an #FbMqttMessage. + */ +GType +fb_mqtt_message_get_type(void); + +/** + * fb_mqtt_error_quark: + * + * Gets the #GQuark of the domain of MQTT errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_mqtt_error_quark(void); + +/** + * fb_mqtt_new: + * @gc: The #PurpleConnection. + * + * Creates a new #FbMqtt. The returned #FbMqtt should be freed with + * #g_object_unref() when no longer needed. + * + * Returns: The new #FbMqtt. + */ +FbMqtt * +fb_mqtt_new(PurpleConnection *gc); + +/** + * fb_mqtt_close: + * @mqtt: The #FbMqtt. + * + * Closes the MQTT without sending the `DISCONNECT` message. The #FbMqtt + * may be reopened after calling this. + */ +void +fb_mqtt_close(FbMqtt *mqtt); + +/** + * fb_mqtt_error: + * @mqtt: The #FbMqtt. + * @error: The #FbMqttError. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Emits an #FbMqttError and closes the #FbMqtt. + */ +void +fb_mqtt_error(FbMqtt *mqtt, FbMqttError error, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_mqtt_read: + * @mqtt: The #FbMqtt. + * @msg: The #FbMqttMessage. + * + * Reads an #FbMqttMessage into the #FbMqtt for processing. + */ +void +fb_mqtt_read(FbMqtt *mqtt, FbMqttMessage *msg); + +/** + * fb_mqtt_write: + * @mqtt: The #FbMqtt. + * @msg: The #FbMqttMessage. + * + * Writes an #FbMqttMessage to the wire. + */ +void +fb_mqtt_write(FbMqtt *mqtt, FbMqttMessage *msg); + +/** + * fb_mqtt_open: + * @mqtt: The #FbMqtt. + * @host: The host name. + * @port: The port. + * + * Opens an SSL connection to the remote server. + */ +void +fb_mqtt_open(FbMqtt *mqtt, const gchar *host, gint port); + +/** + * fb_mqtt_connect: + * @mqtt: The #FbMqtt. + * @flags: The #FbMqttConnectFlags. + * @pload: The payload. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_CONNECT. + */ +void +fb_mqtt_connect(FbMqtt *mqtt, guint8 flags, const GByteArray *pload); + +/** + * fb_mqtt_connected: + * @mqtt: The #FbMqtt. + * @error: #TRUE to error with no connection, otherwise #FALSE. + * + * Determines the connection state of the #FbMqtt, and optionally emits + * an error. + * + * Returns: #TRUE if the #FbMqtt is connected, otherwise #FALSE. + */ +gboolean +fb_mqtt_connected(FbMqtt *mqtt, gboolean error); + +/** + * fb_mqtt_disconnect: + * @mqtt: The #FbMqtt. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_DISCONNECT, and closes + * the connection. + */ +void +fb_mqtt_disconnect(FbMqtt *mqtt); + +/** + * fb_mqtt_publish: + * @mqtt: The #FbMqtt. + * @topic: The topic. + * @pload: The payload. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_PUBLISH. + */ +void +fb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, const GByteArray *pload); + +/** + * fb_mqtt_subscribe: + * @mqtt: The #FbMqtt. + * @topic1: The first topic. + * @qos1: The first QoS. + * @...: The %NULL-terminated list of topic/QoS pairs. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_SUBSCRIBE. + */ +void +fb_mqtt_subscribe(FbMqtt *mqtt, const gchar *topic1, guint16 qos1, ...) + G_GNUC_NULL_TERMINATED; + +/** + * fb_mqtt_unsubscribe: + * @mqtt: The #FbMqtt. + * @topic1: The first topic. + * @...: The %NULL-terminated list of topics. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE. + */ +void +fb_mqtt_unsubscribe(FbMqtt *mqtt, const gchar *topic1, ...) + G_GNUC_NULL_TERMINATED; + +/** + * fb_mqtt_message_new: + * @type: The #FbMqttMessageType. + * @flags: The #FbMqttMessageFlags. + * + * Creates a new #FbMqttMessage. The returned #FbMqttMessage should be + * freed with #g_object_unref() when no longer needed. + * + * Returns: The new #FbMqttMessage. + */ +FbMqttMessage * +fb_mqtt_message_new(FbMqttMessageType type, FbMqttMessageFlags flags); + +/** + * fb_mqtt_message_new_bytes: + * @bytes: The #GByteArray. + * + * Creates a new #FbMqttMessage from a #GByteArray. The returned + * #FbMqttMessage should be freed with #g_object_unref() when no + * longer needed. + * + * Returns: The new #FbMqttMessage. + */ +FbMqttMessage * +fb_mqtt_message_new_bytes(GByteArray *bytes); + +/** + * fb_mqtt_message_reset: + * @msg: The #FbMqttMessage. + * + * Resets an #FbMqttMessage. This resets the cursor position, and + * removes any sort of fixed header. + */ +void +fb_mqtt_message_reset(FbMqttMessage *msg); + +/** + * fb_mqtt_message_bytes: + * @msg: The #FbMqttMessage. + * + * Formats the internal #GByteArray of the #FbMqttMessage with the + * required fixed header. This resets the cursor position. + * + * Returns: The internal #GByteArray. + */ +const GByteArray * +fb_mqtt_message_bytes(FbMqttMessage *msg); + +/** + * fb_mqtt_message_read: + * @msg: The #FbMqttMessage. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Reads data from the #FbMqttMessage into a buffer. If @data is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the data was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read(FbMqttMessage *msg, gpointer data, guint size); + +/** + * fb_mqtt_message_read_r: + * @msg: The #FbMqttMessage. + * @bytes: The #GByteArray. + * + * Reads the remaining data from the #FbMqttMessage into a #GByteArray. + * This is useful for obtaining the payload of a message. + * + * Returns: #TRUE if the data was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_r(FbMqttMessage *msg, GByteArray *bytes); + +/** + * fb_mqtt_message_read_byte: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads an 8-bit integer value from the #FbMqttMessage. If @value is + * #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_byte(FbMqttMessage *msg, guint8 *value); + +/** + * fb_mqtt_message_read_mid: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads a message identifier from the #FbMqttMessage. If @value is + * #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_mid(FbMqttMessage *msg, guint16 *value); + +/** + * fb_mqtt_message_read_u16: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads a 16-bit integer value from the #FbMqttMessage. If @value is + * #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_u16(FbMqttMessage *msg, guint16 *value); + +/** + * fb_mqtt_message_read_str: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads a string value from the #FbMqttMessage. The value returned to + * @value should be freed with #g_free() when no longer needed. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_str(FbMqttMessage *msg, gchar **value); + +/** + * fb_mqtt_message_write: + * @msg: The #FbMqttMessage. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Writes data to the #FbMqttMessage. + */ +void +fb_mqtt_message_write(FbMqttMessage *msg, gconstpointer data, guint size); + +/** + * fb_mqtt_message_write_byte: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes an 8-bit integer value to the #FbMqttMessage. + */ +void +fb_mqtt_message_write_byte(FbMqttMessage *msg, guint8 value); + +/** + * fb_mqtt_message_write_mid: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes a message identifier to the #FbMqttMessage. This increments + * @value for the next message. + */ +void +fb_mqtt_message_write_mid(FbMqttMessage *msg, guint16 *value); + +/** + * fb_mqtt_message_write_u16: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes a 16-bit integer value to the #FbMqttMessage. + */ +void +fb_mqtt_message_write_u16(FbMqttMessage *msg, guint16 value); + +/** + * fb_mqtt_message_write_str: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes a string value to the #FbMqttMessage. + */ +void +fb_mqtt_message_write_str(FbMqttMessage *msg, const gchar *value); + +#endif /* _FACEBOOK_MQTT_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/thrift.c b/pidgin/libpurple/protocols/facebook/thrift.c new file mode 100644 index 0000000..d33a94e --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/thrift.c @@ -0,0 +1,699 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include <string.h> + +#include "thrift.h" + +struct _FbThriftPrivate +{ + GByteArray *bytes; + gboolean internal; + guint offset; + guint pos; + guint lastbool; +}; + +G_DEFINE_TYPE(FbThrift, fb_thrift, G_TYPE_OBJECT); + +static void +fb_thrift_dispose(GObject *obj) +{ + FbThriftPrivate *priv = FB_THRIFT(obj)->priv; + + if (priv->internal) { + g_byte_array_free(priv->bytes, TRUE); + } +} + +static void +fb_thrift_class_init(FbThriftClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_thrift_dispose; + g_type_class_add_private(klass, sizeof (FbThriftPrivate)); +} + +static void +fb_thrift_init(FbThrift *thft) +{ + FbThriftPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(thft, FB_TYPE_THRIFT, + FbThriftPrivate); + thft->priv = priv; +} + +FbThrift * +fb_thrift_new(GByteArray *bytes, guint offset) +{ + FbThrift *thft; + FbThriftPrivate *priv; + + thft = g_object_new(FB_TYPE_THRIFT, NULL); + priv = thft->priv; + + if (bytes != NULL) { + priv->bytes = bytes; + priv->offset = offset; + priv->pos = offset; + } else { + priv->bytes = g_byte_array_new(); + priv->internal = TRUE; + } + + return thft; +} + +const GByteArray * +fb_thrift_get_bytes(FbThrift *thft) +{ + FbThriftPrivate *priv; + + g_return_val_if_fail(FB_IS_THRIFT(thft), NULL); + priv = thft->priv; + return priv->bytes; +} + +guint +fb_thrift_get_pos(FbThrift *thft) +{ + FbThriftPrivate *priv; + + g_return_val_if_fail(FB_IS_THRIFT(thft), 0); + priv = thft->priv; + return priv->pos; +} + +void +fb_thrift_set_pos(FbThrift *thft, guint pos) +{ + FbThriftPrivate *priv; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + priv->pos = pos; +} + +void +fb_thrift_reset(FbThrift *thft) +{ + FbThriftPrivate *priv; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + priv->pos = priv->offset; +} + +gboolean +fb_thrift_read(FbThrift *thft, gpointer data, guint size) +{ + FbThriftPrivate *priv; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + priv = thft->priv; + + if ((priv->pos + size) > priv->bytes->len) { + return FALSE; + } + + if ((data != NULL) && (size > 0)) { + memcpy(data, priv->bytes->data + priv->pos, size); + } + + priv->pos += size; + return TRUE; +} + +gboolean +fb_thrift_read_bool(FbThrift *thft, gboolean *value) +{ + FbThriftPrivate *priv; + guint8 byte; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + priv = thft->priv; + + if ((priv->lastbool & 0x03) != 0x01) { + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + if (value != NULL) { + *value = (byte & 0x0F) == 0x01; + } + + priv->lastbool = 0; + return TRUE; + } + + if (value != NULL) { + *value = ((priv->lastbool & 0x04) >> 2) != 0; + } + + priv->lastbool = 0; + return TRUE; +} + +gboolean +fb_thrift_read_byte(FbThrift *thft, guint8 *value) +{ + return fb_thrift_read(thft, value, sizeof *value); +} + +gboolean +fb_thrift_read_dbl(FbThrift *thft, gdouble *value) +{ + gint64 i64; + + /* Almost always 8, but check anyways */ + static const gsize size = MIN(sizeof value, sizeof i64); + + if (!fb_thrift_read_i64(thft, &i64)) { + return FALSE; + } + + if (value != NULL) { + memcpy(value, &i64, size); + } + + return TRUE; +} + +gboolean +fb_thrift_read_i16(FbThrift *thft, gint16 *value) +{ + gint64 i64; + + if (!fb_thrift_read_i64(thft, &i64)) { + return FALSE; + } + + if (value != NULL) { + *value = i64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_vi16(FbThrift *thft, guint16 *value) +{ + guint64 u64; + + if (!fb_thrift_read_vi64(thft, &u64)) { + return FALSE; + } + + if (value != NULL) { + *value = u64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_i32(FbThrift *thft, gint32 *value) +{ + gint64 i64; + + if (!fb_thrift_read_i64(thft, &i64)) { + return FALSE; + } + + if (value != NULL) { + *value = i64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_vi32(FbThrift *thft, guint32 *value) +{ + guint64 u64; + + if (!fb_thrift_read_vi64(thft, &u64)) { + return FALSE; + } + + if (value != NULL) { + *value = u64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_i64(FbThrift *thft, gint64 *value) +{ + guint64 u64; + + if (!fb_thrift_read_vi64(thft, &u64)) { + return FALSE; + } + + if (value != NULL) { + /* Convert from zigzag to integer */ + *value = (u64 >> 0x01) ^ -(u64 & 0x01); + } + + return TRUE; +} + +gboolean +fb_thrift_read_vi64(FbThrift *thft, guint64 *value) +{ + guint i = 0; + guint8 byte; + guint64 u64 = 0; + + do { + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + u64 |= ((guint64) (byte & 0x7F)) << i; + i += 7; + } while ((byte & 0x80) == 0x80); + + if (value != NULL) { + *value = u64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_str(FbThrift *thft, gchar **value) +{ + guint8 *data; + guint32 size; + + if (!fb_thrift_read_vi32(thft, &size)) { + return FALSE; + } + + if (value != NULL) { + data = g_new(guint8, size + 1); + data[size] = 0; + } else { + data = NULL; + } + + if (!fb_thrift_read(thft, data, size)) { + g_free(data); + return FALSE; + } + + if (value != NULL) { + *value = (gchar *) data; + } + + return TRUE; +} + +gboolean +fb_thrift_read_field(FbThrift *thft, FbThriftType *type, gint16 *id, + gint16 lastid) +{ + FbThriftPrivate *priv; + gint16 i16; + guint8 byte; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + g_return_val_if_fail(type != NULL, FALSE); + g_return_val_if_fail(id != NULL, FALSE); + priv = thft->priv; + + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + if (byte == FB_THRIFT_TYPE_STOP) { + *type = FB_THRIFT_TYPE_STOP; + return FALSE; + } + + *type = fb_thrift_ct2t(byte & 0x0F); + i16 = (byte & 0xF0) >> 4; + + if (i16 == 0) { + if (!fb_thrift_read_i16(thft, id)) { + return FALSE; + } + } else { + *id = lastid + i16; + } + + if (*type == FB_THRIFT_TYPE_BOOL) { + priv->lastbool = 0x01; + + if ((byte & 0x0F) == 0x01) { + priv->lastbool |= 0x01 << 2; + } + } + + return TRUE; +} + +gboolean +fb_thrift_read_stop(FbThrift *thft) +{ + guint8 byte; + + return fb_thrift_read_byte(thft, &byte) && + (byte == FB_THRIFT_TYPE_STOP); +} + +gboolean +fb_thrift_read_isstop(FbThrift *thft) +{ + FbThriftPrivate *priv; + guint8 byte; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + priv = thft->priv; + + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + priv->pos--; + return byte == FB_THRIFT_TYPE_STOP; +} + +gboolean +fb_thrift_read_list(FbThrift *thft, FbThriftType *type, guint *size) +{ + guint8 byte; + guint32 u32; + + g_return_val_if_fail(type != NULL, FALSE); + g_return_val_if_fail(size != NULL, FALSE); + + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + *type = fb_thrift_ct2t(byte & 0x0F); + *size = (byte & 0xF0) >> 4; + + if (*size == 0x0F) { + if (!fb_thrift_read_vi32(thft, &u32)) { + return FALSE; + } + + *size = u32; + } + + return TRUE; +} + +gboolean +fb_thrift_read_map(FbThrift *thft, FbThriftType *ktype, FbThriftType *vtype, + guint *size) +{ + gint32 i32; + guint8 byte; + + g_return_val_if_fail(ktype != NULL, FALSE); + g_return_val_if_fail(vtype != NULL, FALSE); + g_return_val_if_fail(size != NULL, FALSE); + + if (!fb_thrift_read_i32(thft, &i32)) { + return FALSE; + } + + if (i32 != 0) { + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + *ktype = fb_thrift_ct2t((byte & 0xF0) >> 4); + *vtype = fb_thrift_ct2t(byte & 0x0F); + } else { + *ktype = 0; + *vtype = 0; + } + + *size = i32; + return TRUE; +} + +gboolean +fb_thrift_read_set(FbThrift *thft, FbThriftType *type, guint *size) +{ + return fb_thrift_read_list(thft, type, size); +} + +void +fb_thrift_write(FbThrift *thft, gconstpointer data, guint size) +{ + FbThriftPrivate *priv; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + + g_byte_array_append(priv->bytes, data, size); + priv->pos += size; +} + +void +fb_thrift_write_bool(FbThrift *thft, gboolean value) +{ + FbThriftPrivate *priv; + guint pos; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + + if ((priv->lastbool & 0x03) != 0x02) { + fb_thrift_write_byte(thft, value ? 0x01 : 0x02); + return; + } + + pos = priv->lastbool >> 3; + priv->lastbool = 0; + + if ((pos >= priv->offset) && (pos < priv->bytes->len)) { + priv->bytes->data[pos] &= ~0x0F; + priv->bytes->data[pos] |= value ? 0x01 : 0x02; + } +} + +void +fb_thrift_write_byte(FbThrift *thft, guint8 value) +{ + fb_thrift_write(thft, &value, sizeof value); +} + +void +fb_thrift_write_dbl(FbThrift *thft, gdouble value) +{ + gint64 i64; + + /* Almost always 8, but check anyways */ + static const gsize size = MIN(sizeof value, sizeof i64); + + memcpy(&i64, &value, size); + fb_thrift_write_i64(thft, i64); +} + +void +fb_thrift_write_i16(FbThrift *thft, gint16 value) +{ + fb_thrift_write_i64(thft, value); +} + +void +fb_thrift_write_vi16(FbThrift *thft, guint16 value) +{ + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_i32(FbThrift *thft, gint32 value) +{ + value = (value << 1) ^ (value >> 31); + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_vi32(FbThrift *thft, guint32 value) +{ + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_i64(FbThrift *thft, gint64 value) +{ + value = (value << 1) ^ (value >> 63); + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_vi64(FbThrift *thft, guint64 value) +{ + gboolean last; + guint8 byte; + + do { + last = (value & ~0x7F) == 0; + byte = value & 0x7F; + + if (!last) { + byte |= 0x80; + value >>= 7; + } + + fb_thrift_write_byte(thft, byte); + } while (!last); +} + +void +fb_thrift_write_str(FbThrift *thft, const gchar *value) +{ + guint32 size; + + g_return_if_fail(value != NULL); + + size = strlen(value); + fb_thrift_write_vi32(thft, size); + fb_thrift_write(thft, value, size); +} + +void +fb_thrift_write_field(FbThrift *thft, FbThriftType type, gint16 id, + gint16 lastid) +{ + FbThriftPrivate *priv; + gint16 diff; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + + if (type == FB_THRIFT_TYPE_BOOL) { + priv->lastbool = (priv->pos << 3) | 0x02; + } + + type = fb_thrift_t2ct(type); + diff = id - lastid; + + if ((id <= lastid) || (diff > 0x0F)) { + fb_thrift_write_byte(thft, type); + fb_thrift_write_i16(thft, id); + } else { + fb_thrift_write_byte(thft, (diff << 4) | type); + } +} + +void +fb_thrift_write_stop(FbThrift *thft) +{ + fb_thrift_write_byte(thft, FB_THRIFT_TYPE_STOP); +} + +void +fb_thrift_write_list(FbThrift *thft, FbThriftType type, guint size) +{ + type = fb_thrift_t2ct(type); + + if (size <= 14) { + fb_thrift_write_byte(thft, (size << 4) | type); + return; + } + + fb_thrift_write_vi32(thft, size); + fb_thrift_write_byte(thft, 0xF0 | type); +} + +void +fb_thrift_write_map(FbThrift *thft, FbThriftType ktype, FbThriftType vtype, + guint size) +{ + if (size == 0) { + fb_thrift_write_byte(thft, 0); + return; + } + + ktype = fb_thrift_t2ct(ktype); + vtype = fb_thrift_t2ct(vtype); + + fb_thrift_write_vi32(thft, size); + fb_thrift_write_byte(thft, (ktype << 4) | vtype); +} + +void +fb_thrift_write_set(FbThrift *thft, FbThriftType type, guint size) +{ + fb_thrift_write_list(thft, type, size); +} + +guint8 +fb_thrift_t2ct(FbThriftType type) +{ + static const guint8 types[] = { + [FB_THRIFT_TYPE_STOP] = 0, + [FB_THRIFT_TYPE_VOID] = 0, + [FB_THRIFT_TYPE_BOOL] = 2, + [FB_THRIFT_TYPE_BYTE] = 3, + [FB_THRIFT_TYPE_DOUBLE] = 7, + [5] = 0, + [FB_THRIFT_TYPE_I16] = 4, + [7] = 0, + [FB_THRIFT_TYPE_I32] = 5, + [9] = 0, + [FB_THRIFT_TYPE_I64] = 6, + [FB_THRIFT_TYPE_STRING] = 8, + [FB_THRIFT_TYPE_STRUCT] = 12, + [FB_THRIFT_TYPE_MAP] = 11, + [FB_THRIFT_TYPE_SET] = 10, + [FB_THRIFT_TYPE_LIST] = 9 + }; + + g_return_val_if_fail(type < G_N_ELEMENTS(types), 0); + return types[type]; +} + +FbThriftType +fb_thrift_ct2t(guint8 type) +{ + static const guint8 types[] = { + [0] = FB_THRIFT_TYPE_STOP, + [1] = FB_THRIFT_TYPE_BOOL, + [2] = FB_THRIFT_TYPE_BOOL, + [3] = FB_THRIFT_TYPE_BYTE, + [4] = FB_THRIFT_TYPE_I16, + [5] = FB_THRIFT_TYPE_I32, + [6] = FB_THRIFT_TYPE_I64, + [7] = FB_THRIFT_TYPE_DOUBLE, + [8] = FB_THRIFT_TYPE_STRING, + [9] = FB_THRIFT_TYPE_LIST, + [10] = FB_THRIFT_TYPE_SET, + [11] = FB_THRIFT_TYPE_MAP, + [12] = FB_THRIFT_TYPE_STRUCT + }; + + g_return_val_if_fail(type < G_N_ELEMENTS(types), 0); + return types[type]; +} diff --git a/pidgin/libpurple/protocols/facebook/thrift.h b/pidgin/libpurple/protocols/facebook/thrift.h new file mode 100644 index 0000000..0da34ca --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/thrift.h @@ -0,0 +1,604 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_THRIFT_H_ +#define _FACEBOOK_THRIFT_H_ + +/** + * SECTION:thrift + * @section_id: facebook-thrift + * @short_description: <filename>thrift.h</filename> + * @title: Thrift Reader/Writer + * + * The Thrift reader/writer. + */ + +#include <glib.h> +#include <glib-object.h> + +#define FB_TYPE_THRIFT (fb_thrift_get_type()) +#define FB_THRIFT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_THRIFT, FbThrift)) +#define FB_THRIFT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_THRIFT, FbThriftClass)) +#define FB_IS_THRIFT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_THRIFT)) +#define FB_IS_THRIFT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_THRIFT)) +#define FB_THRIFT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_THRIFT, FbThriftClass)) + +typedef struct _FbThrift FbThrift; +typedef struct _FbThriftClass FbThriftClass; +typedef struct _FbThriftPrivate FbThriftPrivate; + +/** + * FbThriftType: + * @FB_THRIFT_TYPE_STOP: A stopper for certain types. + * @FB_THRIFT_TYPE_VOID: A void or empty value. + * @FB_THRIFT_TYPE_BOOL: A boolean (#TRUE or #FALSE). + * @FB_THRIFT_TYPE_BYTE: A signed 8-bit integer. + * @FB_THRIFT_TYPE_DOUBLE: A 64-bit floating point number. + * @FB_THRIFT_TYPE_I16: A signed 16-bit integer. + * @FB_THRIFT_TYPE_I32: A signed 32-bit integer. + * @FB_THRIFT_TYPE_I64: A signed 64-bit integer. + * @FB_THRIFT_TYPE_STRING: A UTF-8 encoded string. + * @FB_THRIFT_TYPE_STRUCT: A set of typed fields. + * @FB_THRIFT_TYPE_MAP: A map of unique keys to values. + * @FB_THRIFT_TYPE_SET: A unique set of values. + * @FB_THRIFT_TYPE_LIST: A ordered list of values. + * @FB_THRIFT_TYPE_ENUM: A 32-bit enumerated list. + * @FB_THRIFT_TYPE_UNKNOWN: An unknown type. + * + * The Thrift data types. + */ +typedef enum +{ + FB_THRIFT_TYPE_STOP = 0, + FB_THRIFT_TYPE_VOID = 1, + FB_THRIFT_TYPE_BOOL = 2, + FB_THRIFT_TYPE_BYTE = 3, + FB_THRIFT_TYPE_DOUBLE = 4, + FB_THRIFT_TYPE_I16 = 6, + FB_THRIFT_TYPE_I32 = 8, + FB_THRIFT_TYPE_I64 = 10, + FB_THRIFT_TYPE_STRING = 11, + FB_THRIFT_TYPE_STRUCT = 12, + FB_THRIFT_TYPE_MAP = 13, + FB_THRIFT_TYPE_SET = 14, + FB_THRIFT_TYPE_LIST = 15, + FB_THRIFT_TYPE_ENUM = 16, + + FB_THRIFT_TYPE_UNKNOWN +} FbThriftType; + +/** + * FbThrift: + * + * Represents a reader/writer for compact Thrift data. + */ +struct _FbThrift +{ + /*< private >*/ + GObject parent; + FbThriftPrivate *priv; +}; + +/** + * FbThriftClass: + * + * The base class for all #FbThrift's. + */ +struct _FbThriftClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_thrift_get_type: + * + * Returns: The #GType for an #FbThrift. + */ +GType +fb_thrift_get_type(void); + +/** + * fb_thrift_new: + * @bytes: The #GByteArray to read or write. + * @offset: The offset in bytes of the data in @bytes. + * + * Creates a new #FbThrift. The returned #FbThrift should be freed with + * #g_object_unref() when no longer needed. This will optionally use a + * #GByteArray at an offset, rather than a newly created and internal + * #GByteArray. + * + * Returns: The new #FbThrift. + */ +FbThrift * +fb_thrift_new(GByteArray *bytes, guint offset); + +/** + * fb_thrift_get_bytes: + * @thft: The #FbThrift. + * + * Gets the underlying #GByteArray of an #FbThrift. + * + * Returns: The #GByteArray. + */ +const GByteArray * +fb_thrift_get_bytes(FbThrift *thft); + +/** + * fb_thrift_get_pos: + * @thft: The #FbThrift. + * + * Gets the cursor position of an #FbThrift. + * + * Returns: The cursor position. + */ +guint +fb_thrift_get_pos(FbThrift *thft); + +/** + * fb_thrift_set_pos: + * @thft: The #FbThrift. + * @pos: The position. + * + * Sets the cursor position of an #FbThrift. + * + * Returns: The #GByteArray. + */ +void +fb_thrift_set_pos(FbThrift *thft, guint pos); + +/** + * fb_thrift_reset: + * @thft: The #FbThrift. + * + * Resets the cursor position of an #FbThrift. + * + * Returns: The #GByteArray. + */ +void +fb_thrift_reset(FbThrift *thft); + +/** + * fb_thrift_read: + * @thft: The #FbThrift. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Reads data from the #FbThrift into a buffer. If @data is #NULL, this + * will simply advance the cursor position. + * + * Returns: #TRUE if the data was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read(FbThrift *thft, gpointer data, guint size); + +/** + * fb_thrift_read_bool: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a boolean value from the #FbThrift. If @value is #NULL, this + * will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_bool(FbThrift *thft, gboolean *value); + +/** + * fb_thrift_read_byte: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads an 8-bit integer value from the #FbThrift. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_byte(FbThrift *thft, guint8 *value); + +/** + * fb_thrift_read_dbl: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 64-bit floating point value from the #FbThrift. If @value + * is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_dbl(FbThrift *thft, gdouble *value); + +/** + * fb_thrift_read_i16: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a signed 16-bit integer value from the #FbThrift. This will + * convert the integer from the zig-zag format. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_i16(FbThrift *thft, gint16 *value); + +/** + * fb_thrift_read_vi16: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 16-bit integer value from the #FbThrift. This reads the raw + * integer value without converting it from the zig-zag format. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_vi16(FbThrift *thft, guint16 *value); + +/** + * fb_thrift_read_i32: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a signed 32-bit integer value from the #FbThrift. This will + * convert the integer from the zig-zag format. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_i32(FbThrift *thft, gint32 *value); + +/** + * fb_thrift_read_vi32: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 32-bit integer value from the #FbThrift. This reads the raw + * integer value without converting it from the zig-zag format. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_vi32(FbThrift *thft, guint32 *value); + +/** + * fb_thrift_read_i64: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a signed 64-bit integer value from the #FbThrift. This will + * convert the integer from the zig-zag format. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_i64(FbThrift *thft, gint64 *value); + +/** + * fb_thrift_read_vi64: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 64-bit integer value from the #FbThrift. This reads the raw + * integer value without converting it from the zig-zag format. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_vi64(FbThrift *thft, guint64 *value); + +/** + * fb_thrift_read_str: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a string value from the #FbThrift. The value returned to + * @value should be freed with #g_free() when no longer needed. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_str(FbThrift *thft, gchar **value); + +/** + * fb_thrift_read_field: + * @thft: The #FbThrift. + * @type: The return location for the #FbThriftType. + * @id: The return location for the identifier. + * @lastid: The identifier of the previous field. + * + * Reads a field header from the #FbThrift. + * + * Returns: #TRUE if the field header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_field(FbThrift *thft, FbThriftType *type, gint16 *id, + gint16 lastid); + +/** + * fb_thrift_read_stop: + * @thft: The #FbThrift. + * + * Reads a field stop from the #FbThrift. + * + * Returns: #TRUE if the field stop was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_stop(FbThrift *thft); + +/** + * fb_thrift_read_isstop: + * @thft: The #FbThrift. + * + * Determines if the next byte of the #FbThrift is a field stop. + * + * Returns: #TRUE if the next byte is a field stop, otherwise #FALSE. + */ +gboolean +fb_thrift_read_isstop(FbThrift *thft); + +/** + * fb_thrift_read_list: + * @thft: The #FbThrift. + * @type: The return location for the #FbThriftType. + * @size: The return location for the size. + * + * Reads a list header from the #FbThrift. + * + * Returns: #TRUE if the list header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_list(FbThrift *thft, FbThriftType *type, guint *size); + +/** + * fb_thrift_read_map: + * @thft: The #FbThrift. + * @ktype: The return location for the key #FbThriftType. + * @vtype: The return location for the value #FbThriftType. + * @size: The return location for the size. + * + * Reads a map header from the #FbThrift. + * + * Returns: #TRUE if the map header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_map(FbThrift *thft, FbThriftType *ktype, FbThriftType *vtype, + guint *size); + +/** + * fb_thrift_read_set: + * @thft: The #FbThrift. + * @type: The return location for the #FbThriftType. + * @size: The return location for the size. + * + * Reads a set header from the #FbThrift. + * + * Returns: #TRUE if the set header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_set(FbThrift *thft, FbThriftType *type, guint *size); + +/** + * fb_thrift_write: + * @thft: The #FbThrift. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Writes data to the #FbThrift. + */ +void +fb_thrift_write(FbThrift *thft, gconstpointer data, guint size); + +/** + * fb_thrift_write_bool: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a boolean value to the #FbThrift. + */ +void +fb_thrift_write_bool(FbThrift *thft, gboolean value); + +/** + * fb_thrift_write_byte: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes an 8-bit integer value to the #FbThrift. + */ +void +fb_thrift_write_byte(FbThrift *thft, guint8 value); + +/** + * fb_thrift_write_dbl: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 64-bit floating point value to the #FbThrift. + */ +void +fb_thrift_write_dbl(FbThrift *thft, gdouble value); + +/** + * fb_thrift_write_i16: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a signed 16-bit integer value to the #FbThrift. This will + * convert the integer to the zig-zag format. + */ +void +fb_thrift_write_i16(FbThrift *thft, gint16 value); + +/** + * fb_thrift_write_vi16: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 16-bit integer value to the #FbThrift. This writes the raw + * integer value without converting it to the zig-zag format. + */ +void +fb_thrift_write_vi16(FbThrift *thft, guint16 value); + +/** + * fb_thrift_write_i32: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a signed 32-bit integer value to the #FbThrift. This will + * convert the integer to the zig-zag format. + */ +void +fb_thrift_write_i32(FbThrift *thft, gint32 value); + +/** + * fb_thrift_write_vi32: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 32-bit integer value to the #FbThrift. This writes the raw + * integer value without converting it to the zig-zag format. + */ +void +fb_thrift_write_vi32(FbThrift *thft, guint32 value); + +/** + * fb_thrift_write_i64: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a signed 64-bit integer value to the #FbThrift. This will + * convert the integer to the zig-zag format. + */ +void +fb_thrift_write_i64(FbThrift *thft, gint64 value); + +/** + * fb_thrift_write_vi64: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 64-bit integer value to the #FbThrift. This writes the raw + * integer value without converting it to the zig-zag format. + */ +void +fb_thrift_write_vi64(FbThrift *thft, guint64 value); + +/** + * fb_thrift_write_str: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a string value to the #FbThrift. + */ +void +fb_thrift_write_str(FbThrift *thft, const gchar *value); + +/** + * fb_thrift_write_field: + * @thft: The #FbThrift. + * @type: The #FbThriftType. + * @id: The identifier. + * @lastid: The identifier of the previous field. + * + * Writes a field header to the #FbThrift. + */ +void +fb_thrift_write_field(FbThrift *thft, FbThriftType type, gint16 id, + gint16 lastid); + +/** + * fb_thrift_write_stop: + * @thft: The #FbThrift. + * + * Writes a field stop to the #FbThrift. + */ +void +fb_thrift_write_stop(FbThrift *thft); + +/** + * fb_thrift_write_list: + * @thft: The #FbThrift. + * @type: The #FbThriftType. + * @size: The size. + * + * Writes a list header to the #FbThrift. + */ +void +fb_thrift_write_list(FbThrift *thft, FbThriftType type, guint size); + +/** + * fb_thrift_write_map: + * @thft: The #FbThrift. + * @ktype: The key #FbThriftType. + * @vtype: The value #FbThriftType. + * @size: The size. + * + * Writes a map header to the #FbThrift. + */ +void +fb_thrift_write_map(FbThrift *thft, FbThriftType ktype, FbThriftType vtype, + guint size); + +/** + * fb_thrift_write_set: + * @thft: The #FbThrift. + * @type: The #FbThriftType. + * @size: The size. + * + * Writes a set header to the #FbThrift. + */ +void +fb_thrift_write_set(FbThrift *thft, FbThriftType type, guint size); + +/** + * fb_thrift_t2ct: + * @type: The #FbThriftType. + * + * Converts a #FbThriftType to a compact type. + * + * Return: The equivalent compact type. + */ +guint8 +fb_thrift_t2ct(FbThriftType type); + +/** + * fb_thrift_ct2t: + * @type: The compact type. + * + * Converts a compact type to an #FbThriftType. + * + * Return: The equivalent #FbThriftType. + */ +FbThriftType +fb_thrift_ct2t(guint8 type); + +#endif /* _FACEBOOK_THRIFT_H_ */ diff --git a/pidgin/libpurple/protocols/facebook/util.c b/pidgin/libpurple/protocols/facebook/util.c new file mode 100644 index 0000000..72890a5 --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/util.c @@ -0,0 +1,566 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include <gio/gio.h> +#include <stdarg.h> +#include <string.h> + +#include "buddylist.h" +#include "conversations.h" +#include "glibcompat.h" +#include "message.h" +#include "request.h" +#include "server.h" + +#include "util.h" + +GQuark +fb_util_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-util-error-quark"); + } + + return q; +} + +PurpleBuddy * +fb_util_account_find_buddy(PurpleAccount *acct, PurpleChatConversation *chat, + const gchar *search, GError **error) +{ + const gchar *alias; + const gchar *name; + GSList *buddies; + GSList *l; + guint retc; + PurpleBuddy *ret = NULL; + + g_return_val_if_fail(PURPLE_IS_ACCOUNT(acct), NULL); + g_return_val_if_fail(search != NULL, NULL); + + buddies = purple_blist_find_buddies(acct, NULL); + + for (retc = 0, l = buddies; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + alias = purple_buddy_get_alias(l->data); + + if ((chat != NULL) && + !purple_chat_conversation_has_user(chat, name)) + { + continue; + } + + if (g_ascii_strcasecmp(name, search) == 0) { + ret = l->data; + retc++; + } + + if (g_ascii_strcasecmp(alias, search) == 0) { + ret = l->data; + retc++; + } + } + + if (retc == 0) { + g_set_error(error, FB_UTIL_ERROR, FB_UTIL_ERROR_GENERAL, + _("Buddy %s not found"), search); + } else if (retc > 1) { + g_set_error(error, FB_UTIL_ERROR, FB_UTIL_ERROR_GENERAL, + _("Buddy name %s is ambiguous"), search); + ret = NULL; + } + + g_slist_free(buddies); + return ret; +} + +void +fb_util_debug(PurpleDebugLevel level, const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(level, format, ap); + va_end(ap); +} + +void +fb_util_vdebug(PurpleDebugLevel level, const gchar *format, va_list ap) +{ + gboolean unsafe; + gboolean verbose; + gchar *str; + + g_return_if_fail(format != NULL); + + unsafe = (level & FB_UTIL_DEBUG_FLAG_UNSAFE) != 0; + verbose = (level & FB_UTIL_DEBUG_FLAG_VERBOSE) != 0; + + if ((unsafe && !purple_debug_is_unsafe()) || + (verbose && !purple_debug_is_verbose())) + { + return; + } + + /* Ensure all local flags are removed */ + level &= ~FB_UTIL_DEBUG_FLAG_ALL; + + str = g_strdup_vprintf(format, ap); + purple_debug(level, "facebook", "%s\n", str); + g_free(str); +} + +void +fb_util_debug_misc(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_MISC, format, ap); + va_end(ap); +} + +void +fb_util_debug_info(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_INFO, format, ap); + va_end(ap); +} + +void +fb_util_debug_warning(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_WARNING, format, ap); + va_end(ap); +} + +void +fb_util_debug_error(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_ERROR, format, ap); + va_end(ap); +} + +void +fb_util_debug_fatal(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_FATAL, format, ap); + va_end(ap); +} + +void +fb_util_debug_hexdump(PurpleDebugLevel level, const GByteArray *bytes, + const gchar *format, ...) +{ + gchar c; + guint i; + guint j; + GString *gstr; + va_list ap; + + static const gchar *indent = " "; + + g_return_if_fail(bytes != NULL); + + if (format != NULL) { + va_start(ap, format); + fb_util_vdebug(level, format, ap); + va_end(ap); + } + + gstr = g_string_sized_new(80); + + for (i = 0; i < bytes->len; i += 16) { + g_string_append_printf(gstr, "%s%08x ", indent, i); + + for (j = 0; j < 16; j++) { + if ((i + j) < bytes->len) { + g_string_append_printf(gstr, "%02x ", + bytes->data[i + j]); + } else { + g_string_append(gstr, " "); + } + + if (j == 7) { + g_string_append_c(gstr, ' '); + } + } + + g_string_append(gstr, " |"); + + for (j = 0; (j < 16) && ((i + j) < bytes->len); j++) { + c = bytes->data[i + j]; + + if (!g_ascii_isprint(c) || g_ascii_isspace(c)) { + c = '.'; + } + + g_string_append_c(gstr, c); + } + + g_string_append_c(gstr, '|'); + fb_util_debug(level, "%s", gstr->str); + g_string_erase(gstr, 0, -1); + } + + g_string_append_printf(gstr, "%s%08x", indent, i); + fb_util_debug(level, "%s", gstr->str); + g_string_free(gstr, TRUE); +} + +gchar * +fb_util_get_locale(void) +{ + const gchar * const *langs; + const gchar *lang; + gchar *chr; + guint i; + + static const gchar chrs[] = {'.', '@'}; + + langs = g_get_language_names(); + lang = langs[0]; + + if (purple_strequal(lang, "C")) { + return g_strdup("en_US"); + } + + for (i = 0; i < G_N_ELEMENTS(chrs); i++) { + chr = strchr(lang, chrs[i]); + + if (chr != NULL) { + return g_strndup(lang, chr - lang); + } + } + + return g_strdup(lang); +} + +gchar * +fb_util_rand_alnum(guint len) +{ + gchar *ret; + GRand *rand; + guint i; + guint j; + + static const gchar chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789"; + static const gsize charc = G_N_ELEMENTS(chars) - 1; + + g_return_val_if_fail(len > 0, NULL); + rand = g_rand_new(); + ret = g_new(gchar, len + 1); + + for (i = 0; i < len; i++) { + j = g_rand_int_range(rand, 0, charc); + ret[i] = chars[j]; + } + + ret[len] = 0; + g_rand_free(rand); + return ret; +} + +static void +fb_util_request_buddy_ok(gpointer *mata, PurpleRequestFields *fields) +{ + FbUtilRequestBuddyFunc func = mata[0]; + GList *l; + GList *select; + gpointer data = mata[2]; + GSList *ret = NULL; + PurpleBuddy *bdy; + PurpleRequestField *field; + + if (func == NULL) { + g_free(mata); + return; + } + + field = purple_request_fields_get_field(fields, "buddy"); + select = purple_request_field_list_get_selected(field); + + for (l = select; l != NULL; l = l->next) { + bdy = purple_request_field_list_get_data(field, l->data); + ret = g_slist_prepend(ret, bdy); + } + + ret = g_slist_reverse(ret); + func(ret, data); + + g_slist_free(ret); + g_free(mata); +} + +static void +fb_util_request_buddy_cancel(gpointer *mata, PurpleRequestFields *fields) +{ + FbUtilRequestBuddyFunc func = mata[1]; + gpointer data = mata[2]; + + if (func != NULL) { + func(NULL, data); + } + + g_free(mata); +} + +gpointer +fb_util_request_buddy(PurpleConnection *gc, const gchar *title, + const gchar *primary, const gchar *secondary, + GSList *select, gboolean multi, GCallback ok_cb, + GCallback cancel_cb, gpointer data) +{ + const gchar *alias; + const gchar *name; + gchar *str; + GList *items = NULL; + gpointer *mata; + GSList *buddies; + GSList *l; + PurpleAccount *acct; + PurpleRequestCommonParameters *cpar; + PurpleRequestField *field; + PurpleRequestFieldGroup *group; + PurpleRequestFields *fields; + + mata = g_new0(gpointer, 3); + mata[0] = ok_cb; + mata[1] = cancel_cb; + mata[2] = data; + + acct = purple_connection_get_account(gc); + buddies = purple_blist_find_buddies(acct, NULL); + buddies = g_slist_sort(buddies, (GCompareFunc) g_ascii_strcasecmp); + + fields = purple_request_fields_new(); + group = purple_request_field_group_new(NULL); + purple_request_fields_add_group(fields, group); + + field = purple_request_field_list_new("buddy", NULL); + purple_request_field_list_set_multi_select(field, multi); + purple_request_field_set_required(field, TRUE); + purple_request_field_group_add_field(group, field); + + for (l = buddies; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + alias = purple_buddy_get_alias(l->data); + str = g_strdup_printf("%s (%s)", alias, name); + purple_request_field_list_add_icon(field, str, NULL, l->data); + g_free(str); + } + + for (l = select; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + alias = purple_buddy_get_alias(l->data); + str = g_strdup_printf("%s (%s)", alias, name); + items = g_list_append(items, str); + } + + purple_request_field_list_set_selected(field, items); + g_slist_free(buddies); + g_list_free_full(items, g_free); + + cpar = purple_request_cpar_from_connection(gc); + return purple_request_fields(gc, title, primary, secondary, fields, + _("Ok"), + G_CALLBACK(fb_util_request_buddy_ok), + _("Cancel"), + G_CALLBACK(fb_util_request_buddy_cancel), + cpar, mata); +} + +void +fb_util_serv_got_im(PurpleConnection *gc, const gchar *who, const gchar *text, + PurpleMessageFlags flags, guint64 timestamp) +{ + const gchar *name; + PurpleAccount *acct; + PurpleIMConversation *conv; + PurpleMessage *msg; + + if (!(flags & PURPLE_MESSAGE_SEND)) { + purple_serv_got_im(gc, who, text, flags, timestamp); + return; + } + + acct = purple_connection_get_account(gc); + conv = purple_conversations_find_im_with_account(who, acct); + + if (conv == NULL) { + conv = purple_im_conversation_new(acct, who); + } + + name = purple_account_get_username(acct); + msg = purple_message_new_outgoing(name, text, flags); + purple_message_set_time(msg, timestamp); + purple_conversation_write_message(PURPLE_CONVERSATION(conv), msg); +} + +void +fb_util_serv_got_chat_in(PurpleConnection *gc, gint id, const gchar *who, + const gchar *text, PurpleMessageFlags flags, + guint64 timestamp) +{ + const gchar *name; + PurpleAccount *acct; + PurpleChatConversation *conv; + PurpleMessage *msg; + + if (!(flags & PURPLE_MESSAGE_SEND)) { + purple_serv_got_chat_in(gc, id, who, flags, text, timestamp); + return; + } + + acct = purple_connection_get_account(gc); + conv = purple_conversations_find_chat(gc, id); + + name = purple_account_get_username(acct); + msg = purple_message_new_outgoing(name, text, flags); + purple_message_set_time(msg, timestamp); + purple_conversation_write_message(PURPLE_CONVERSATION(conv), msg); +} + +gboolean +fb_util_strtest(const gchar *str, GAsciiType type) +{ + gsize i; + gsize size; + guchar c; + + g_return_val_if_fail(str != NULL, FALSE); + size = strlen(str); + + for (i = 0; i < size; i++) { + c = (guchar) str[i]; + + if ((g_ascii_table[c] & type) == 0) { + return FALSE; + } + } + + return TRUE; +} + +gboolean +fb_util_zlib_test(const GByteArray *bytes) +{ + guint8 b0; + guint8 b1; + + g_return_val_if_fail(bytes != NULL, FALSE); + + if (bytes->len < 2) { + return FALSE; + } + + b0 = *(bytes->data + 0); + b1 = *(bytes->data + 1); + + return ((((b0 << 8) | b1) % 31) == 0) && /* Check the header */ + ((b0 & 0x0F) == 8 /* Z_DEFLATED */); /* Check the method */ +} + +static GByteArray * +fb_util_zlib_conv(GConverter *conv, const GByteArray *bytes, GError **error) +{ + GByteArray *ret; + GConverterResult res; + gsize cize = 0; + gsize rize; + gsize wize; + guint8 data[1024]; + + ret = g_byte_array_new(); + + while (TRUE) { + rize = 0; + wize = 0; + + res = g_converter_convert(conv, + bytes->data + cize, + bytes->len - cize, + data, sizeof data, + G_CONVERTER_INPUT_AT_END, + &rize, &wize, error); + + switch (res) { + case G_CONVERTER_CONVERTED: + g_byte_array_append(ret, data, wize); + cize += rize; + break; + + case G_CONVERTER_ERROR: + g_byte_array_free(ret, TRUE); + return NULL; + + case G_CONVERTER_FINISHED: + g_byte_array_append(ret, data, wize); + return ret; + + default: + break; + } + } +} + +GByteArray * +fb_util_zlib_deflate(const GByteArray *bytes, GError **error) +{ + GByteArray *ret; + GZlibCompressor *conv; + + conv = g_zlib_compressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB, -1); + ret = fb_util_zlib_conv(G_CONVERTER(conv), bytes, error); + g_object_unref(conv); + return ret; +} + +GByteArray * +fb_util_zlib_inflate(const GByteArray *bytes, GError **error) +{ + GByteArray *ret; + GZlibDecompressor *conv; + + conv = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB); + ret = fb_util_zlib_conv(G_CONVERTER(conv), bytes, error); + g_object_unref(conv); + return ret; +} diff --git a/pidgin/libpurple/protocols/facebook/util.h b/pidgin/libpurple/protocols/facebook/util.h new file mode 100644 index 0000000..2a1d5eb --- /dev/null +++ b/pidgin/libpurple/protocols/facebook/util.h @@ -0,0 +1,350 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_UTIL_H_ +#define _FACEBOOK_UTIL_H_ + +/** + * SECTION:util + * @section_id: facebook-util + * @short_description: <filename>util.h</filename> + * @title: General Utilities + * + * The general utilities. + */ + +#include <glib.h> + +#include <libpurple/util.h> + +#include "account.h" +#include "conversationtypes.h" +#include "debug.h" +#include "connection.h" +#include "conversation.h" + +/** + * FB_UTIL_DEBUG_INFO: + * + * Shortcut #PurpleDebugLevel for unsafe and verbose info messages. + */ +#define FB_UTIL_DEBUG_INFO ( \ + PURPLE_DEBUG_INFO | \ + FB_UTIL_DEBUG_FLAG_UNSAFE | \ + FB_UTIL_DEBUG_FLAG_VERBOSE \ + ) + +/** + * FB_UTIL_ERROR: + * + * The #GQuark of the domain of utility errors. + */ +#define FB_UTIL_ERROR fb_util_error_quark() + +/** + * FbUtilRequestBuddyFunc: + * @buddies: The list of #PurpleBuddy's. + * @data: The user-defined data. + * + * The callback for requested buddies. + */ +typedef void (*FbUtilRequestBuddyFunc) (GSList *buddies, gpointer data); + +/** + * FbUtilDebugFlags: + * @FB_UTIL_DEBUG_FLAG_UNSAFE: The message is unsafe. + * @FB_UTIL_DEBUG_FLAG_VERBOSE: The message is verbose. + * @FB_UTIL_DEBUG_FLAG_ALL: All of the flags. + * + * The debugging message flags. These flags are inserted on top of + * a #PurpleDebugLevel. + */ +typedef enum +{ + FB_UTIL_DEBUG_FLAG_UNSAFE = 1 << 25, + FB_UTIL_DEBUG_FLAG_VERBOSE = 1 << 26, + FB_UTIL_DEBUG_FLAG_ALL = 3 << 25 +} FbUtilDebugFlags; + +/** + * FbUtilError: + * @FB_UTIL_ERROR_GENERAL: General failure. + * + * The error codes for the #FB_UTIL_ERROR domain. + */ +typedef enum +{ + FB_UTIL_ERROR_GENERAL +} FbUtilError; + +/** + * fb_util_error_quark: + * + * Gets the #GQuark of the domain of utility errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_util_error_quark(void); + +/** + * fb_util_account_find_buddy: + * @acct: The #PurpleAccount. + * @chat: The #PurpleChatConversation. + * @name: The name of the buddy. + * @error: The return location for the #GError or #NULL. + * + * Finds a buddy by their name or alias. + * + * Returns: The #PurpleBuddy if found, otherwise #NULL. + */ +PurpleBuddy * +fb_util_account_find_buddy(PurpleAccount *acct, PurpleChatConversation *chat, + const gchar *name, GError **error); + +/** + * fb_util_debug: + * @level: The #PurpleDebugLevel. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message. If the messages is unsafe or verbose, + * apply the appropriate #FbUtilDebugFlags. + */ +void +fb_util_debug(PurpleDebugLevel level, const gchar *format, ...) + G_GNUC_PRINTF(2, 3); + +/** + * fb_util_vdebug: + * @level: The #PurpleDebugLevel. + * @format: The format string literal. + * @ap: The #va_list. + * + * Logs a debugging message. If the messages is unsafe or verbose, + * apply the appropriate #FbUtilDebugFlags. + */ +void +fb_util_vdebug(PurpleDebugLevel level, const gchar *format, va_list ap); + +/** + * fb_util_debug_misc: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_MISC. + */ +void +fb_util_debug_misc(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_info: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_INFO. + */ +void +fb_util_debug_info(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_warning: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_WARNING. + */ +void +fb_util_debug_warning(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_error: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_ERROR. + */ +void +fb_util_debug_error(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_fatal: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_FATAL. + */ +void +fb_util_debug_fatal(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_hexdump: + * @level: The #PurpleDebugLevel. + * @bytes: The #GByteArray. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a hexdump of a #GByteArray. If the messages is unsafe or + * verbose, apply the appropriate #FbUtilDebugFlags. + */ +void +fb_util_debug_hexdump(PurpleDebugLevel level, const GByteArray *bytes, + const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_util_get_locale: + * + * Gets the locale string (ex: en_US) from the system. The returned + * string should be freed with #g_free() when no longer needed. + * + * Returns: The locale string. + */ +gchar * +fb_util_get_locale(void); + +/** + * fb_util_rand_alnum: + * @len: The length of the string. + * + * Gets a random alphanumeric (A-Za-z0-9) string. This function should + * *not* be relied on for cryptographic operations. The returned string + * should be freed with #g_free() when no longer needed. + * + * Returns: The alphanumeric string. + */ +gchar * +fb_util_rand_alnum(guint len); + +/** + * fb_util_request_buddy: + * @gc: The #PurpleConnection. + * @title: The title of the message or #NULL. + * @primary: The main point of the message or #NULL. + * @secondary: The secondary information or #NULL. + * @select: A #GSList of selected buddies or #NULL. + * @multi: #TRUE to for multiple buddy selections, otherwise #FALSE. + * @ok_cb: The callback for the `OK` button or #NULL. + * @cancel_cb: The callback for the `Cancel` button or #NULL. + * @data: The user-defined data. + * + * Displays a buddy list selection form. + * + * Returns: The UI-specific handle. + */ +gpointer +fb_util_request_buddy(PurpleConnection *gc, const gchar *title, + const gchar *primary, const gchar *secondary, + GSList *select, gboolean multi, GCallback ok_cb, + GCallback cancel_cb, gpointer data); + +/** + * fb_util_serv_got_im: + * @gc: The #PurpleConnection. + * @who: The message sender or receiver. + * @text: The message text. + * @flags: The #PurpleMessageFlags. + * @timestamp: The message timestamp. + * + * Handles an incoming IM message. This function is special in that it + * handles self messages. This function determines the direction of the + * message from the #PurpleMessageFlags. + */ +void +fb_util_serv_got_im(PurpleConnection *gc, const gchar *who, const gchar *text, + PurpleMessageFlags flags, guint64 timestamp); + +/** + * fb_util_serv_got_chat_in: + * @gc: The #PurpleConnection. + * @id: The id of the chat. + * @who: The message sender or receiver. + * @text: The message text. + * @flags: The #PurpleMessageFlags. + * @timestamp: The message timestamp. + * + * Handles an incoming chat message. This function is special in that + * it handles self messages. This function determines the direction of + * the message from the #PurpleMessageFlags. + */ +void +fb_util_serv_got_chat_in(PurpleConnection *gc, gint id, const gchar *who, + const gchar *text, PurpleMessageFlags flags, + guint64 timestamp); + +/** + * fb_util_strtest: + * @str: The string. + * @type: The #GAsciiType. + * + * Tests if the string only contains characters allowed by the + * #GAsciiType. More than one type can be specified by ORing the types + * together. + * + * Returns: #TRUE if the string only contains characters allowed by the + * #GAsciiType, otherwise #FALSE. + */ +gboolean +fb_util_strtest(const gchar *str, GAsciiType type); + +/** + * fb_util_zlib_test: + * @bytes: The #GByteArray. + * + * Tests if the #GByteArray is zlib compressed. + * + * Returns: #TRUE if the #GByteArray is compressed, otherwise #FALSE. + */ +gboolean +fb_util_zlib_test(const GByteArray *bytes); + +/** + * fb_util_zlib_deflate: + * @bytes: The #GByteArray. + * @error: The return location for the #GError or #NULL. + * + * Deflates a #GByteArray with zlib. The returned #GByteArray should be + * freed with #g_byte_array_free() when no longer needed. + * + * Returns: The deflated #GByteArray or #NULL on error. + */ +GByteArray * +fb_util_zlib_deflate(const GByteArray *bytes, GError **error); + +/** + * fb_util_zlib_inflate: + * @bytes: The #GByteArray. + * @error: The return location for the #GError or #NULL. + * + * Inflates a #GByteArray with zlib. The returned #GByteArray should be + * freed with #g_byte_array_free() when no longer needed. + * + * Returns: The inflated #GByteArray or #NULL on error. + */ +GByteArray * +fb_util_zlib_inflate(const GByteArray *bytes, GError **error); + +#endif /* _FACEBOOK_UTIL_H_ */ |