Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dequis/purple-facebook.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author/ <>2021-02-16 21:42:53 +0300
committer/ <>2021-02-16 21:42:53 +0300
commit25cbf6d1524b823d710cdd323d001cc15be72d58 (patch)
treefe00a9c19960ece9688a3adcbfaa8c15ca43f82b
parent1a6711f83d62e374ca2bed41fd4ca25b6bc654a2 (diff)
parenta13cb8b1b85089c823917d4bf2b89a89f067393e (diff)
merged hg history of libpurple
-rw-r--r--pidgin/AUTHORS146
-rw-r--r--pidgin/COPYING339
-rw-r--r--pidgin/COPYRIGHT643
-rw-r--r--pidgin/libpurple/glibcompat.h112
-rw-r--r--pidgin/libpurple/http.c3406
-rw-r--r--pidgin/libpurple/http.h964
-rw-r--r--pidgin/libpurple/protocols/facebook/Makefile.am52
-rw-r--r--pidgin/libpurple/protocols/facebook/Makefile.mingw94
-rw-r--r--pidgin/libpurple/protocols/facebook/api.c3501
-rw-r--r--pidgin/libpurple/protocols/facebook/api.h1006
-rw-r--r--pidgin/libpurple/protocols/facebook/data.c608
-rw-r--r--pidgin/libpurple/protocols/facebook/data.h398
-rw-r--r--pidgin/libpurple/protocols/facebook/facebook.c1713
-rw-r--r--pidgin/libpurple/protocols/facebook/facebook.h84
-rw-r--r--pidgin/libpurple/protocols/facebook/http.c438
-rw-r--r--pidgin/libpurple/protocols/facebook/http.h371
-rw-r--r--pidgin/libpurple/protocols/facebook/id.h131
-rw-r--r--pidgin/libpurple/protocols/facebook/json.c676
-rw-r--r--pidgin/libpurple/protocols/facebook/json.h517
-rw-r--r--pidgin/libpurple/protocols/facebook/meson.build35
-rw-r--r--pidgin/libpurple/protocols/facebook/mqtt.c1063
-rw-r--r--pidgin/libpurple/protocols/facebook/mqtt.h609
-rw-r--r--pidgin/libpurple/protocols/facebook/thrift.c699
-rw-r--r--pidgin/libpurple/protocols/facebook/thrift.h604
-rw-r--r--pidgin/libpurple/protocols/facebook/util.c566
-rw-r--r--pidgin/libpurple/protocols/facebook/util.h350
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_ */