diff options
-rw-r--r-- | debian/copyright | 378 | ||||
-rw-r--r-- | doc/user-guide/commands.xml | 20 | ||||
-rw-r--r-- | lib/Makefile | 2 | ||||
-rw-r--r-- | lib/http_client.c | 179 | ||||
-rw-r--r-- | lib/http_client.h | 40 | ||||
-rw-r--r-- | lib/json.c | 743 | ||||
-rw-r--r-- | lib/json.h | 192 | ||||
-rw-r--r-- | lib/json_util.c | 62 | ||||
-rw-r--r-- | lib/json_util.h | 35 | ||||
-rw-r--r-- | lib/oauth2.c | 81 | ||||
-rw-r--r-- | lib/ssl_gnutls.c | 18 | ||||
-rw-r--r-- | nick.c | 4 | ||||
-rw-r--r-- | protocols/msn/msn.c | 1 | ||||
-rw-r--r-- | protocols/msn/ns.c | 4 | ||||
-rw-r--r-- | protocols/nogaim.c | 23 | ||||
-rw-r--r-- | protocols/nogaim.h | 2 | ||||
-rw-r--r-- | protocols/twitter/twitter.c | 304 | ||||
-rw-r--r-- | protocols/twitter/twitter.h | 18 | ||||
-rw-r--r-- | protocols/twitter/twitter_http.c | 40 | ||||
-rw-r--r-- | protocols/twitter/twitter_http.h | 12 | ||||
-rw-r--r-- | protocols/twitter/twitter_lib.c | 792 | ||||
-rw-r--r-- | protocols/twitter/twitter_lib.h | 45 |
22 files changed, 2041 insertions, 954 deletions
diff --git a/debian/copyright b/debian/copyright index 42f70d03..9002c219 100644 --- a/debian/copyright +++ b/debian/copyright @@ -7,8 +7,17 @@ Authors: Wilmer van der Gaast, Sjoerd Hemminga, Jelmer Vernooij, Maurits Dijkstra, Geert Mulders, Miklos Vajna and others. Mainly Copyright 2002-2012 Wilmer van der Gaast. -Some parts are borrowed from Gaim (version 0.58) <http://gaim.sf.net/>. -For the copyrights on those parts, please read the Gaim source code. + + +Bits of third party code, also GPLed: +* Some parts (mostly protocols/oscar/) are borrowed from Gaim (version + 0.58), now known as Pidgin <http://www.pidgin.im/>. +* protocols/yahoo/ is libyahoo2 <http://libyahoo2.sf.net/>. + +Other license (but GPL-compatible): +* lib/json.[ch] is from <https://github.com/udp/json-parser> and licensed + under the modified BSD license. + BitlBee License: @@ -32,362 +41,15 @@ BitlBee License: The SGML-formatted documentation is written by Jelmer Vernooij -<jelmer@nl.linux.org> under the GNU Free Documentation License: +<jelmer@samba.org> under the GNU Free Documentation License: ============================================================================ - GNU Free Documentation License - Version 1.1, March 2000 - - Copyright (C) 2000 Free Software Foundation, Inc. - 51 Franklin St, 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. - - -0. PREAMBLE - -The purpose of this License is to make a manual, textbook, or other -written document "free" in the sense of freedom: to assure everyone -the effective freedom to copy and redistribute it, with or without -modifying it, either commercially or noncommercially. Secondarily, -this License preserves for the author and publisher a way to get -credit for their work, while not being considered responsible for -modifications made by others. - -This License is a kind of "copyleft", which means that derivative -works of the document must themselves be free in the same sense. It -complements the GNU General Public License, which is a copyleft -license designed for free software. - -We have designed this License in order to use it for manuals for free -software, because free software needs free documentation: a free -program should come with manuals providing the same freedoms that the -software does. But this License is not limited to software manuals; -it can be used for any textual work, regardless of subject matter or -whether it is published as a printed book. We recommend this License -principally for works whose purpose is instruction or reference. - - -1. APPLICABILITY AND DEFINITIONS - -This License applies to any manual or other work that contains a -notice placed by the copyright holder saying it can be distributed -under the terms of this License. The "Document", below, refers to any -such manual or work. Any member of the public is a licensee, and is -addressed as "you". - -A "Modified Version" of the Document means any work containing the -Document or a portion of it, either copied verbatim, or with -modifications and/or translated into another language. - -A "Secondary Section" is a named appendix or a front-matter section of -the Document that deals exclusively with the relationship of the -publishers or authors of the Document to the Document's overall subject -(or to related matters) and contains nothing that could fall directly -within that overall subject. (For example, if the Document is in part a -textbook of mathematics, a Secondary Section may not explain any -mathematics.) The relationship could be a matter of historical -connection with the subject or with related matters, or of legal, -commercial, philosophical, ethical or political position regarding -them. - -The "Invariant Sections" are certain Secondary Sections whose titles -are designated, as being those of Invariant Sections, in the notice -that says that the Document is released under this License. - -The "Cover Texts" are certain short passages of text that are listed, -as Front-Cover Texts or Back-Cover Texts, in the notice that says that -the Document is released under this License. - -A "Transparent" copy of the Document means a machine-readable copy, -represented in a format whose specification is available to the -general public, whose contents can be viewed and edited directly and -straightforwardly with generic text editors or (for images composed of -pixels) generic paint programs or (for drawings) some widely available -drawing editor, and that is suitable for input to text formatters or -for automatic translation to a variety of formats suitable for input -to text formatters. A copy made in an otherwise Transparent file -format whose markup has been designed to thwart or discourage -subsequent modification by readers is not Transparent. A copy that is -not "Transparent" is called "Opaque". - -Examples of suitable formats for Transparent copies include plain -ASCII without markup, Texinfo input format, LaTeX input format, SGML -or XML using a publicly available DTD, and standard-conforming simple -HTML designed for human modification. Opaque formats include -PostScript, PDF, proprietary formats that can be read and edited only -by proprietary word processors, SGML or XML for which the DTD and/or -processing tools are not generally available, and the -machine-generated HTML produced by some word processors for output -purposes only. - -The "Title Page" means, for a printed book, the title page itself, -plus such following pages as are needed to hold, legibly, the material -this License requires to appear in the title page. For works in -formats which do not have any title page as such, "Title Page" means -the text near the most prominent appearance of the work's title, -preceding the beginning of the body of the text. - - -2. VERBATIM COPYING - -You may copy and distribute the Document in any medium, either -commercially or noncommercially, provided that this License, the -copyright notices, and the license notice saying this License applies -to the Document are reproduced in all copies, and that you add no other -conditions whatsoever to those of this License. You may not use -technical measures to obstruct or control the reading or further -copying of the copies you make or distribute. However, you may accept -compensation in exchange for copies. If you distribute a large enough -number of copies you must also follow the conditions in section 3. - -You may also lend copies, under the same conditions stated above, and -you may publicly display copies. - - -3. COPYING IN QUANTITY - -If you publish printed copies of the Document numbering more than 100, -and the Document's license notice requires Cover Texts, you must enclose -the copies in covers that carry, clearly and legibly, all these Cover -Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on -the back cover. Both covers must also clearly and legibly identify -you as the publisher of these copies. The front cover must present -the full title with all words of the title equally prominent and -visible. You may add other material on the covers in addition. -Copying with changes limited to the covers, as long as they preserve -the title of the Document and satisfy these conditions, can be treated -as verbatim copying in other respects. - -If the required texts for either cover are too voluminous to fit -legibly, you should put the first ones listed (as many as fit -reasonably) on the actual cover, and continue the rest onto adjacent -pages. - -If you publish or distribute Opaque copies of the Document numbering -more than 100, you must either include a machine-readable Transparent -copy along with each Opaque copy, or state in or with each Opaque copy -a publicly-accessible computer-network location containing a complete -Transparent copy of the Document, free of added material, which the -general network-using public has access to download anonymously at no -charge using public-standard network protocols. If you use the latter -option, you must take reasonably prudent steps, when you begin -distribution of Opaque copies in quantity, to ensure that this -Transparent copy will remain thus accessible at the stated location -until at least one year after the last time you distribute an Opaque -copy (directly or through your agents or retailers) of that edition to -the public. - -It is requested, but not required, that you contact the authors of the -Document well before redistributing any large number of copies, to give -them a chance to provide you with an updated version of the Document. - - -4. MODIFICATIONS - -You may copy and distribute a Modified Version of the Document under -the conditions of sections 2 and 3 above, provided that you release -the Modified Version under precisely this License, with the Modified -Version filling the role of the Document, thus licensing distribution -and modification of the Modified Version to whoever possesses a copy -of it. In addition, you must do these things in the Modified Version: - -A. Use in the Title Page (and on the covers, if any) a title distinct - from that of the Document, and from those of previous versions - (which should, if there were any, be listed in the History section - of the Document). You may use the same title as a previous version - if the original publisher of that version gives permission. -B. List on the Title Page, as authors, one or more persons or entities - responsible for authorship of the modifications in the Modified - Version, together with at least five of the principal authors of the - Document (all of its principal authors, if it has less than five). -C. State on the Title page the name of the publisher of the - Modified Version, as the publisher. -D. Preserve all the copyright notices of the Document. -E. Add an appropriate copyright notice for your modifications - adjacent to the other copyright notices. -F. Include, immediately after the copyright notices, a license notice - giving the public permission to use the Modified Version under the - terms of this License, in the form shown in the Addendum below. -G. Preserve in that license notice the full lists of Invariant Sections - and required Cover Texts given in the Document's license notice. -H. Include an unaltered copy of this License. -I. Preserve the section entitled "History", and its title, and add to - it an item stating at least the title, year, new authors, and - publisher of the Modified Version as given on the Title Page. If - there is no section entitled "History" in the Document, create one - stating the title, year, authors, and publisher of the Document as - given on its Title Page, then add an item describing the Modified - Version as stated in the previous sentence. -J. Preserve the network location, if any, given in the Document for - public access to a Transparent copy of the Document, and likewise - the network locations given in the Document for previous versions - it was based on. These may be placed in the "History" section. - You may omit a network location for a work that was published at - least four years before the Document itself, or if the original - publisher of the version it refers to gives permission. -K. In any section entitled "Acknowledgements" or "Dedications", - preserve the section's title, and preserve in the section all the - substance and tone of each of the contributor acknowledgements - and/or dedications given therein. -L. Preserve all the Invariant Sections of the Document, - unaltered in their text and in their titles. Section numbers - or the equivalent are not considered part of the section titles. -M. Delete any section entitled "Endorsements". Such a section - may not be included in the Modified Version. -N. Do not retitle any existing section as "Endorsements" - or to conflict in title with any Invariant Section. - -If the Modified Version includes new front-matter sections or -appendices that qualify as Secondary Sections and contain no material -copied from the Document, you may at your option designate some or all -of these sections as invariant. To do this, add their titles to the -list of Invariant Sections in the Modified Version's license notice. -These titles must be distinct from any other section titles. - -You may add a section entitled "Endorsements", provided it contains -nothing but endorsements of your Modified Version by various -parties--for example, statements of peer review or that the text has -been approved by an organization as the authoritative definition of a -standard. - -You may add a passage of up to five words as a Front-Cover Text, and a -passage of up to 25 words as a Back-Cover Text, to the end of the list -of Cover Texts in the Modified Version. Only one passage of -Front-Cover Text and one of Back-Cover Text may be added by (or -through arrangements made by) any one entity. If the Document already -includes a cover text for the same cover, previously added by you or -by arrangement made by the same entity you are acting on behalf of, -you may not add another; but you may replace the old one, on explicit -permission from the previous publisher that added the old one. - -The author(s) and publisher(s) of the Document do not by this License -give permission to use their names for publicity for or to assert or -imply endorsement of any Modified Version. - - -5. COMBINING DOCUMENTS - -You may combine the Document with other documents released under this -License, under the terms defined in section 4 above for modified -versions, provided that you include in the combination all of the -Invariant Sections of all of the original documents, unmodified, and -list them all as Invariant Sections of your combined work in its -license notice. - -The combined work need only contain one copy of this License, and -multiple identical Invariant Sections may be replaced with a single -copy. If there are multiple Invariant Sections with the same name but -different contents, make the title of each such section unique by -adding at the end of it, in parentheses, the name of the original -author or publisher of that section if known, or else a unique number. -Make the same adjustment to the section titles in the list of -Invariant Sections in the license notice of the combined work. - -In the combination, you must combine any sections entitled "History" -in the various original documents, forming one section entitled -"History"; likewise combine any sections entitled "Acknowledgements", -and any sections entitled "Dedications". You must delete all sections -entitled "Endorsements." - - -6. COLLECTIONS OF DOCUMENTS - -You may make a collection consisting of the Document and other documents -released under this License, and replace the individual copies of this -License in the various documents with a single copy that is included in -the collection, provided that you follow the rules of this License for -verbatim copying of each of the documents in all other respects. - -You may extract a single document from such a collection, and distribute -it individually under this License, provided you insert a copy of this -License into the extracted document, and follow this License in all -other respects regarding verbatim copying of that document. - - -7. AGGREGATION WITH INDEPENDENT WORKS - -A compilation of the Document or its derivatives with other separate -and independent documents or works, in or on a volume of a storage or -distribution medium, does not as a whole count as a Modified Version -of the Document, provided no compilation copyright is claimed for the -compilation. Such a compilation is called an "aggregate", and this -License does not apply to the other self-contained works thus compiled -with the Document, on account of their being thus compiled, if they -are not themselves derivative works of the Document. - -If the Cover Text requirement of section 3 is applicable to these -copies of the Document, then if the Document is less than one quarter -of the entire aggregate, the Document's Cover Texts may be placed on -covers that surround only the Document within the aggregate. -Otherwise they must appear on covers around the whole aggregate. - - -8. TRANSLATION - -Translation is considered a kind of modification, so you may -distribute translations of the Document under the terms of section 4. -Replacing Invariant Sections with translations requires special -permission from their copyright holders, but you may include -translations of some or all Invariant Sections in addition to the -original versions of these Invariant Sections. You may include a -translation of this License provided that you also include the -original English version of this License. In case of a disagreement -between the translation and the original English version of this -License, the original English version will prevail. - - -9. TERMINATION - -You may not copy, modify, sublicense, or distribute the Document except -as expressly provided for under this License. Any other attempt to -copy, modify, sublicense or distribute the Document 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. - - -10. FUTURE REVISIONS OF THIS LICENSE - -The Free Software Foundation may publish new, revised versions -of the GNU Free Documentation 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. See -http://www.gnu.org/copyleft/. - -Each version of the License is given a distinguishing version number. -If the Document specifies that a particular numbered version of this -License "or any later version" applies to it, you have the option of -following the terms and conditions either of that specified version or -of any later version that has been published (not as a draft) by the -Free Software Foundation. If the Document does not specify a version -number of this License, you may choose any version ever published (not -as a draft) by the Free Software Foundation. - - -ADDENDUM: How to use this License for your documents - -To use this License in a document you have written, include a copy of -the License in the document and put the following copyright and -license notices just after the title page: - - Copyright (c) YEAR YOUR NAME. - Permission is granted to copy, distribute and/or modify this document - under the terms of the GNU Free Documentation License, Version 1.1 - or any later version published by the Free Software Foundation; - with the Invariant Sections being LIST THEIR TITLES, with the - Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. - A copy of the license is included in the section entitled "GNU - Free Documentation License". - -If you have no Invariant Sections, write "with no Invariant Sections" -instead of saying which ones are invariant. If you have no -Front-Cover Texts, write "no Front-Cover Texts" instead of -"Front-Cover Texts being LIST"; likewise for Back-Cover Texts. - -If your document contains nontrivial examples of program code, we -recommend releasing these examples in parallel under your choice of -free software license, such as the GNU General Public License, -to permit their use in free software. +Copyright (c) 2002-2012 Jelmer Vernooij, Wilmer van der Gaast. + +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.1 or +any later version published by the Free Software Foundation; with no +Invariant Sections, with no Front-Cover Texts, and with no Back-Cover +Texts. A copy of the license is included in the section entitled `GNU +Free Documentation License''. ============================================================================ diff --git a/doc/user-guide/commands.xml b/doc/user-guide/commands.xml index 7a4baa73..c5c9689a 100644 --- a/doc/user-guide/commands.xml +++ b/doc/user-guide/commands.xml @@ -834,6 +834,7 @@ <bitlbee-setting name="commands" type="boolean" scope="account"> <default>true</default> + <possible-values>true, false, strict</possible-values> <description> <para> @@ -852,7 +853,7 @@ </variablelist> <para> - Anything that doesn't look like a command will be treated as a tweet. Watch out for typos! :-) + Anything that doesn't look like a command will be treated as a tweet. Watch out for typos, or to avoid this behaviour, you can set this setting to <emphasis>strict</emphasis>, which causes the <emphasis>post</emphasis> command to become mandatory for posting a tweet. </para> </description> </bitlbee-setting> @@ -1022,6 +1023,21 @@ </description> </bitlbee-setting> + + <bitlbee-setting name="stream" type="boolean" scope="account"> + <default>true</default> + + <description> + <para> + For Twitter accounts, this setting enables use of the Streaming API. This automatically gives you incoming DMs as well. + </para> + + <para> + For other Twitter-like services, this setting is not supported. + </para> + </description> + + </bitlbee-setting> <bitlbee-setting name="target_url_length" type="integer" scope="account"> <default>20</default> @@ -1337,7 +1353,7 @@ </bitlbee-setting> <bitlbee-setting name="show_ids" type="boolean" scope="account"> - <default>false</default> + <default>true</default> <description> <para> diff --git a/lib/Makefile b/lib/Makefile index 324ab646..f20b3797 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -12,7 +12,7 @@ _SRCDIR_ := $(_SRCDIR_)lib/ endif # [SH] Program variables -objects = arc.o base64.o $(EVENT_HANDLER) ftutil.o http_client.o ini.o md5.o misc.o oauth.o oauth2.o proxy.o sha1.o $(SSL_CLIENT) url.o xmltree.o +objects = arc.o base64.o $(EVENT_HANDLER) ftutil.o http_client.o ini.o json.o json_util.o md5.o misc.o oauth.o oauth2.o proxy.o sha1.o $(SSL_CLIENT) url.o xmltree.o LFLAGS += -r diff --git a/lib/http_client.c b/lib/http_client.c index 7ed539d0..e368c0dc 100644 --- a/lib/http_client.c +++ b/lib/http_client.c @@ -192,12 +192,13 @@ static gboolean http_ssl_connected( gpointer data, int returncode, void *source, return http_connected( data, req->fd, cond ); } +static gboolean http_handle_headers( struct http_request *req ); + static gboolean http_incoming_data( gpointer data, int source, b_input_condition cond ) { struct http_request *req = data; - int evil_server = 0; - char buffer[2048]; - char *end1, *end2, *s; + char buffer[4096]; + char *s; size_t content_length; int st; @@ -217,12 +218,12 @@ static gboolean http_incoming_data( gpointer data, int source, b_input_condition servers that LOVE to send invalid TLS packets that abort connections! \o/ */ - goto got_reply; + goto eof; } } else if( st == 0 ) { - goto got_reply; + goto eof; } } else @@ -238,28 +239,70 @@ static gboolean http_incoming_data( gpointer data, int source, b_input_condition } else if( st == 0 ) { - goto got_reply; + goto eof; } } - if( st > 0 ) + if( st > 0 && !req->sbuf ) { req->reply_headers = g_realloc( req->reply_headers, req->bytes_read + st + 1 ); memcpy( req->reply_headers + req->bytes_read, buffer, st ); req->bytes_read += st; + + st = 0; + } + + if( st >= 0 && ( req->flags & HTTPC_STREAMING ) ) + { + if( !req->reply_body && + ( strstr( req->reply_headers, "\r\n\r\n" ) || + strstr( req->reply_headers, "\n\n" ) ) ) + { + size_t hlen; + + /* We've now received all headers, so process them once + before we start feeding back data. */ + if( !http_handle_headers( req ) ) + return FALSE; + + hlen = req->reply_body - req->reply_headers; + + req->sblen = req->bytes_read - hlen; + req->sbuf = g_memdup( req->reply_body, req->sblen + 1 ); + req->reply_headers = g_realloc( req->reply_headers, hlen + 1 ); + + req->reply_body = req->sbuf; + } + + if( st > 0 ) + { + int pos = req->reply_body - req->sbuf; + req->sbuf = g_realloc( req->sbuf, req->sblen + st + 1 ); + memcpy( req->sbuf + req->sblen, buffer, st ); + req->bytes_read += st; + req->sblen += st; + req->sbuf[req->sblen] = '\0'; + req->reply_body = req->sbuf + pos; + req->body_size = req->sblen - pos; + } + + if( req->reply_body ) + req->func( req ); } + if( ssl_pending( req->ssl ) ) + return http_incoming_data( data, source, cond ); + /* There will be more! */ req->inpa = b_input_add( req->fd, req->ssl ? ssl_getdirection( req->ssl ) : B_EV_IO_READ, http_incoming_data, req ); - if( ssl_pending( req->ssl ) ) - return http_incoming_data( data, source, cond ); - else - return FALSE; + return FALSE; -got_reply: +eof: + req->flags |= HTTPC_EOF; + /* Maybe if the webserver is overloaded, or when there's bad SSL support... */ if( req->bytes_read == 0 ) @@ -268,8 +311,50 @@ got_reply: goto cleanup; } + if( !( req->flags & HTTPC_STREAMING ) ) + { + /* Returns FALSE if we were redirected, in which case we should abort + and not run any callback yet. */ + if( !http_handle_headers( req ) ) + return FALSE; + } + +cleanup: + if( req->ssl ) + ssl_disconnect( req->ssl ); + else + closesocket( req->fd ); + + if( ( s = get_rfc822_header( req->reply_headers, "Content-Length", 0 ) ) && + sscanf( s, "%zd", &content_length ) == 1 ) + { + if( content_length < req->body_size ) + { + req->status_code = -1; + g_free( req->status_string ); + req->status_string = g_strdup( "Response truncated" ); + } + } + g_free( s ); + + if( getenv( "BITLBEE_DEBUG" ) && req ) + printf( "Finishing HTTP request with status: %s\n", + req->status_string ? req->status_string : "NULL" ); + + req->func( req ); + http_free( req ); + return FALSE; +} + +/* Splits headers and body. Checks result code, in case of 300s it'll handle + redirects. If this returns FALSE, don't call any callbacks! */ +static gboolean http_handle_headers( struct http_request *req ) +{ + char *end1, *end2; + int evil_server = 0; + /* Zero termination is very convenient. */ - req->reply_headers[req->bytes_read] = 0; + req->reply_headers[req->bytes_read] = '\0'; /* Find the separation between headers and body, and keep stupid webservers in mind. */ @@ -288,7 +373,7 @@ got_reply: else { req->status_string = g_strdup( "Malformed HTTP reply" ); - goto cleanup; + return TRUE; } *end1 = 0; @@ -305,7 +390,7 @@ got_reply: if( ( end1 = strchr( req->reply_headers, ' ' ) ) != NULL ) { - if( sscanf( end1 + 1, "%d", &req->status_code ) != 1 ) + if( sscanf( end1 + 1, "%hd", &req->status_code ) != 1 ) { req->status_string = g_strdup( "Can't parse status code" ); req->status_code = -1; @@ -348,7 +433,7 @@ got_reply: if( loc == NULL ) /* We can't handle this redirect... */ { req->status_string = g_strdup( "Can't locate Location: header" ); - goto cleanup; + return TRUE; } loc += 11; @@ -368,7 +453,7 @@ got_reply: req->status_string = g_strdup( "Can't handle recursive redirects" ); - goto cleanup; + return TRUE; } else { @@ -379,7 +464,7 @@ got_reply: s = strstr( loc, "\r\n" ); if( s == NULL ) - goto cleanup; + return TRUE; url = g_new0( url_t, 1 ); *s = 0; @@ -388,7 +473,7 @@ got_reply: { req->status_string = g_strdup( "Malformed redirect URL" ); g_free( url ); - goto cleanup; + return TRUE; } /* Find all headers and, if necessary, the POST request contents. @@ -400,7 +485,7 @@ got_reply: { req->status_string = g_strdup( "Error while rebuilding request string" ); g_free( url ); - goto cleanup; + return TRUE; } /* More or less HTTP/1.0 compliant, from my reading of RFC 2616. @@ -466,7 +551,7 @@ got_reply: { req->status_string = g_strdup( "Connection problem during redirect" ); g_free( new_request ); - goto cleanup; + return TRUE; } g_free( req->request ); @@ -479,35 +564,41 @@ got_reply: return FALSE; } - /* Assume that a closed connection means we're finished, this indeed - breaks with keep-alive connections and faulty connections. */ - /* req->finished = 1; */ + return TRUE; +} -cleanup: - if( req->ssl ) - ssl_disconnect( req->ssl ); - else - closesocket( req->fd ); +void http_flush_bytes( struct http_request *req, size_t len ) +{ + if( len <= 0 || len > req->body_size || !( req->flags & HTTPC_STREAMING ) ) + return; - if( ( s = get_rfc822_header( req->reply_headers, "Content-Length", 0 ) ) && - sscanf( s, "%zd", &content_length ) == 1 ) + req->reply_body += len; + req->body_size -= len; + + if( req->reply_body - req->sbuf >= 512 ) { - if( content_length < req->body_size ) - { - req->status_code = -1; - g_free( req->status_string ); - req->status_string = g_strdup( "Response truncated" ); - } + printf( "Wasting %ld bytes, cleaning up stream buffer\n", req->reply_body - req->sbuf ); + char *new = g_memdup( req->reply_body, req->body_size + 1 ); + g_free( req->sbuf ); + req->reply_body = req->sbuf = new; + req->sblen = req->body_size; } - g_free( s ); +} + +void http_close( struct http_request *req ) +{ + if( !req ) + return; - if( getenv( "BITLBEE_DEBUG" ) && req ) - printf( "Finishing HTTP request with status: %s\n", - req->status_string ? req->status_string : "NULL" ); + if( req->inpa > 0 ) + b_event_remove( req->inpa ); + + if( req->ssl ) + ssl_disconnect( req->ssl ); + else + closesocket( req->fd ); - req->func( req ); http_free( req ); - return FALSE; } static void http_free( struct http_request *req ) @@ -515,6 +606,6 @@ static void http_free( struct http_request *req ) g_free( req->request ); g_free( req->reply_headers ); g_free( req->status_string ); + g_free( req->sbuf ); g_free( req ); } - diff --git a/lib/http_client.h b/lib/http_client.h index 623f17a0..e2c0319a 100644 --- a/lib/http_client.h +++ b/lib/http_client.h @@ -25,23 +25,26 @@ /* http_client allows you to talk (asynchronously, again) to HTTP servers. In the "background" it will send the whole query and wait for a complete - response to come back. Right now it's only used by the MSN Passport - authentication code, but it might be useful for other things too (for - example the AIM usericon patch uses this so icons can be stored on - webservers instead of the local filesystem). + response to come back. Initially written for MS Passport authentication, + but used for many other things now like OAuth and Twitter. - Didn't test this too much, but it seems to work well. Just don't look - at the code that handles HTTP 30x redirects. ;-) The function is - probably not very useful for downloading lots of data since it keeps - everything in a memory buffer until the download is completed (and - can't pass any data or whatever before then). It's very useful for - doing quick requests without blocking the whole program, though. */ + It's very useful for doing quick requests without blocking the whole + program. Unfortunately it doesn't support fancy stuff like HTTP keep- + alives. */ #include <glib.h> #include "ssl_client.h" struct http_request; +typedef enum http_client_flags +{ + HTTPC_STREAMING = 1, + HTTPC_EOF = 2, + + /* Let's reserve 0x1000000+ for lib users. */ +} http_client_flags_t; + /* Your callback function should look like this: */ typedef void (*http_input_function)( struct http_request * ); @@ -52,28 +55,31 @@ struct http_request { char *request; /* The request to send to the server. */ int request_length; /* Its size. */ - int status_code; /* The numeric HTTP status code. (Or -1 + short status_code; /* The numeric HTTP status code. (Or -1 if something really went wrong) */ char *status_string; /* The error text. */ char *reply_headers; char *reply_body; int body_size; /* The number of bytes in reply_body. */ - /* int finished; Set to non-0 if the request was completed - successfully. */ - int redir_ttl; /* You can set it to 0 if you don't want + short redir_ttl; /* You can set it to 0 if you don't want http_client to follow them. */ + http_client_flags_t flags; + http_input_function func; gpointer data; /* Please don't touch the things down here, you shouldn't need them. */ - void *ssl; int fd; int inpa; int bytes_written; int bytes_read; + + /* Used in streaming mode. Caller should read from reply_body. */ + char *sbuf; + size_t sblen; }; /* The _url variant is probably more useful than the raw version. The raw @@ -82,3 +88,7 @@ struct http_request are also supported (using ssl_client). */ struct http_request *http_dorequest( char *host, int port, int ssl, char *request, http_input_function func, gpointer data ); struct http_request *http_dorequest_url( char *url_string, http_input_function func, gpointer data ); + +/* For streaming connections only; flushes len bytes at the start of the buffer. */ +void http_flush_bytes( struct http_request *req, size_t len ); +void http_close( struct http_request *req ); diff --git a/lib/json.c b/lib/json.c new file mode 100644 index 00000000..52e22d0e --- /dev/null +++ b/lib/json.c @@ -0,0 +1,743 @@ + +/* vim: set et ts=3 sw=3 ft=c: + * + * Copyright (C) 2012 James McLaughlin et al. All rights reserved. + * https://github.com/udp/json-parser + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "json.h" + +#ifdef _MSC_VER + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif +#endif + +#ifdef __cplusplus + const struct _json_value json_value_none; /* zero-d by ctor */ +#else + const struct _json_value json_value_none = { 0 }; +#endif + +#include <glib.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <ctype.h> + +typedef unsigned short json_uchar; + +static unsigned char hex_value (json_char c) +{ + if (c >= 'A' && c <= 'F') + return (c - 'A') + 10; + + if (c >= 'a' && c <= 'f') + return (c - 'a') + 10; + + if (c >= '0' && c <= '9') + return c - '0'; + + return 0xFF; +} + +typedef struct +{ + json_settings settings; + int first_pass; + + unsigned long used_memory; + + unsigned int uint_max; + unsigned long ulong_max; + +} json_state; + +static void * json_alloc (json_state * state, unsigned long size, int zero) +{ + void * mem; + + if ((state->ulong_max - state->used_memory) < size) + return 0; + + if (state->settings.max_memory + && (state->used_memory += size) > state->settings.max_memory) + { + return 0; + } + + if (! (mem = zero ? calloc (size, 1) : malloc (size))) + return 0; + + return mem; +} + +static int new_value + (json_state * state, json_value ** top, json_value ** root, json_value ** alloc, json_type type) +{ + json_value * value; + int values_size; + + if (!state->first_pass) + { + value = *top = *alloc; + *alloc = (*alloc)->_reserved.next_alloc; + + if (!*root) + *root = value; + + switch (value->type) + { + case json_array: + + if (! (value->u.array.values = (json_value **) json_alloc + (state, value->u.array.length * sizeof (json_value *), 0)) ) + { + return 0; + } + + break; + + case json_object: + + values_size = sizeof (*value->u.object.values) * value->u.object.length; + + if (! ((*(void **) &value->u.object.values) = json_alloc + (state, values_size + ((unsigned long) value->u.object.values), 0)) ) + { + return 0; + } + + value->_reserved.object_mem = (*(char **) &value->u.object.values) + values_size; + + break; + + case json_string: + + if (! (value->u.string.ptr = (json_char *) json_alloc + (state, (value->u.string.length + 1) * sizeof (json_char), 0)) ) + { + return 0; + } + + break; + + default: + break; + }; + + value->u.array.length = 0; + + return 1; + } + + value = (json_value *) json_alloc (state, sizeof (json_value), 1); + + if (!value) + return 0; + + if (!*root) + *root = value; + + value->type = type; + value->parent = *top; + + if (*alloc) + (*alloc)->_reserved.next_alloc = value; + + *alloc = *top = value; + + return 1; +} + +#define e_off \ + ((int) (i - cur_line_begin)) + +#define whitespace \ + case '\n': ++ cur_line; cur_line_begin = i; \ + case ' ': case '\t': case '\r' + +#define string_add(b) \ + do { if (!state.first_pass) string [string_length] = b; ++ string_length; } while (0); + +const static int + flag_next = 1, flag_reproc = 2, flag_need_comma = 4, flag_seek_value = 8, flag_exponent = 16, + flag_got_exponent_sign = 32, flag_escaped = 64, flag_string = 128, flag_need_colon = 256, + flag_done = 512; + +json_value * json_parse_ex (json_settings * settings, const json_char * json, char * error_buf) +{ + json_char error [128]; + unsigned int cur_line; + const json_char * cur_line_begin, * i; + json_value * top, * root, * alloc = 0; + json_state state; + int flags; + + error[0] = '\0'; + + memset (&state, 0, sizeof (json_state)); + memcpy (&state.settings, settings, sizeof (json_settings)); + + memset (&state.uint_max, 0xFF, sizeof (state.uint_max)); + memset (&state.ulong_max, 0xFF, sizeof (state.ulong_max)); + + state.uint_max -= 8; /* limit of how much can be added before next check */ + state.ulong_max -= 8; + + for (state.first_pass = 1; state.first_pass >= 0; -- state.first_pass) + { + json_uchar uchar; + unsigned char uc_b1, uc_b2, uc_b3, uc_b4; + json_char * string; + unsigned int string_length; + + top = root = 0; + flags = flag_seek_value; + + cur_line = 1; + cur_line_begin = json; + + for (i = json ;; ++ i) + { + json_char b = *i; + + if (flags & flag_done) + { + if (!b) + break; + + switch (b) + { + whitespace: + continue; + + default: + sprintf (error, "%d:%d: Trailing garbage: `%c`", cur_line, e_off, b); + goto e_failed; + }; + } + + if (flags & flag_string) + { + if (!b) + { sprintf (error, "Unexpected EOF in string (at %d:%d)", cur_line, e_off); + goto e_failed; + } + + if (string_length > state.uint_max) + goto e_overflow; + + if (flags & flag_escaped) + { + flags &= ~ flag_escaped; + + switch (b) + { + case 'b': string_add ('\b'); break; + case 'f': string_add ('\f'); break; + case 'n': string_add ('\n'); break; + case 'r': string_add ('\r'); break; + case 't': string_add ('\t'); break; + case 'u': + + if ((uc_b1 = hex_value (*++ i)) == 0xFF || (uc_b2 = hex_value (*++ i)) == 0xFF + || (uc_b3 = hex_value (*++ i)) == 0xFF || (uc_b4 = hex_value (*++ i)) == 0xFF) + { + sprintf (error, "Invalid character value `%c` (at %d:%d)", b, cur_line, e_off); + goto e_failed; + } + + uc_b1 = uc_b1 * 16 + uc_b2; + uc_b2 = uc_b3 * 16 + uc_b4; + + uchar = ((json_char) uc_b1) * 256 + uc_b2; + + if (sizeof (json_char) >= sizeof (json_uchar) || (uc_b1 == 0 && uc_b2 <= 0x7F)) + { + string_add ((json_char) uchar); + break; + } + + if (uchar <= 0x7FF) + { + if (state.first_pass) + string_length += 2; + else + { string [string_length ++] = 0xC0 | ((uc_b2 & 0xC0) >> 6) | ((uc_b1 & 0x7) << 2); + string [string_length ++] = 0x80 | (uc_b2 & 0x3F); + } + + break; + } + + if (state.first_pass) + string_length += 3; + else + { string [string_length ++] = 0xE0 | ((uc_b1 & 0xF0) >> 4); + string [string_length ++] = 0x80 | ((uc_b1 & 0xF) << 2) | ((uc_b2 & 0xC0) >> 6); + string [string_length ++] = 0x80 | (uc_b2 & 0x3F); + } + + break; + + default: + string_add (b); + }; + + continue; + } + + if (b == '\\') + { + flags |= flag_escaped; + continue; + } + + if (b == '"') + { + if (!state.first_pass) + string [string_length] = 0; + + flags &= ~ flag_string; + string = 0; + + switch (top->type) + { + case json_string: + + top->u.string.length = string_length; + flags |= flag_next; + + break; + + case json_object: + + if (state.first_pass) + (*(json_char **) &top->u.object.values) += string_length + 1; + else + { + top->u.object.values [top->u.object.length].name + = (json_char *) top->_reserved.object_mem; + + (*(json_char **) &top->_reserved.object_mem) += string_length + 1; + } + + flags |= flag_seek_value | flag_need_colon; + continue; + + default: + break; + }; + } + else + { + string_add (b); + continue; + } + } + + if (flags & flag_seek_value) + { + switch (b) + { + whitespace: + continue; + + case ']': + + if (top->type == json_array) + flags = (flags & ~ (flag_need_comma | flag_seek_value)) | flag_next; + else if (!state.settings.settings & json_relaxed_commas) + { sprintf (error, "%d:%d: Unexpected ]", cur_line, e_off); + goto e_failed; + } + + break; + + default: + + if (flags & flag_need_comma) + { + if (b == ',') + { flags &= ~ flag_need_comma; + continue; + } + else + { sprintf (error, "%d:%d: Expected , before %c", cur_line, e_off, b); + goto e_failed; + } + } + + if (flags & flag_need_colon) + { + if (b == ':') + { flags &= ~ flag_need_colon; + continue; + } + else + { sprintf (error, "%d:%d: Expected : before %c", cur_line, e_off, b); + goto e_failed; + } + } + + flags &= ~ flag_seek_value; + + switch (b) + { + case '{': + + if (!new_value (&state, &top, &root, &alloc, json_object)) + goto e_alloc_failure; + + continue; + + case '[': + + if (!new_value (&state, &top, &root, &alloc, json_array)) + goto e_alloc_failure; + + flags |= flag_seek_value; + continue; + + case '"': + + if (!new_value (&state, &top, &root, &alloc, json_string)) + goto e_alloc_failure; + + flags |= flag_string; + + string = top->u.string.ptr; + string_length = 0; + + continue; + + case 't': + + if (*(++ i) != 'r' || *(++ i) != 'u' || *(++ i) != 'e') + goto e_unknown_value; + + if (!new_value (&state, &top, &root, &alloc, json_boolean)) + goto e_alloc_failure; + + top->u.boolean = 1; + + flags |= flag_next; + break; + + case 'f': + + if (*(++ i) != 'a' || *(++ i) != 'l' || *(++ i) != 's' || *(++ i) != 'e') + goto e_unknown_value; + + if (!new_value (&state, &top, &root, &alloc, json_boolean)) + goto e_alloc_failure; + + flags |= flag_next; + break; + + case 'n': + + if (*(++ i) != 'u' || *(++ i) != 'l' || *(++ i) != 'l') + goto e_unknown_value; + + if (!new_value (&state, &top, &root, &alloc, json_null)) + goto e_alloc_failure; + + flags |= flag_next; + break; + + default: + + if (isdigit (b) || b == '-') + { + if (!new_value (&state, &top, &root, &alloc, json_integer)) + goto e_alloc_failure; + + flags &= ~ (flag_exponent | flag_got_exponent_sign); + + if (state.first_pass) + continue; + + if (top->type == json_double) + top->u.dbl = g_ascii_strtod (i, (json_char **) &i); + else + top->u.integer = g_ascii_strtoll (i, (json_char **) &i, 10); + + flags |= flag_next | flag_reproc; + } + else + { sprintf (error, "%d:%d: Unexpected %c when seeking value", cur_line, e_off, b); + goto e_failed; + } + }; + }; + } + else + { + switch (top->type) + { + case json_object: + + switch (b) + { + whitespace: + continue; + + case '"': + + if (flags & flag_need_comma && (!state.settings.settings & json_relaxed_commas)) + { + sprintf (error, "%d:%d: Expected , before \"", cur_line, e_off); + goto e_failed; + } + + flags |= flag_string; + + string = (json_char *) top->_reserved.object_mem; + string_length = 0; + + break; + + case '}': + + flags = (flags & ~ flag_need_comma) | flag_next; + break; + + case ',': + + if (flags & flag_need_comma) + { + flags &= ~ flag_need_comma; + break; + } + + default: + + sprintf (error, "%d:%d: Unexpected `%c` in object", cur_line, e_off, b); + goto e_failed; + }; + + break; + + case json_integer: + case json_double: + + if (isdigit (b)) + continue; + + if (b == 'e' || b == 'E') + { + if (!(flags & flag_exponent)) + { + flags |= flag_exponent; + top->type = json_double; + + continue; + } + } + else if (b == '+' || b == '-') + { + if (flags & flag_exponent && !(flags & flag_got_exponent_sign)) + { + flags |= flag_got_exponent_sign; + continue; + } + } + else if (b == '.' && top->type == json_integer) + { + top->type = json_double; + continue; + } + + flags |= flag_next | flag_reproc; + break; + + default: + break; + }; + } + + if (flags & flag_reproc) + { + flags &= ~ flag_reproc; + -- i; + } + + if (flags & flag_next) + { + flags = (flags & ~ flag_next) | flag_need_comma; + + if (!top->parent) + { + /* root value done */ + + flags |= flag_done; + continue; + } + + if (top->parent->type == json_array) + flags |= flag_seek_value; + + if (!state.first_pass) + { + json_value * parent = top->parent; + + switch (parent->type) + { + case json_object: + + parent->u.object.values + [parent->u.object.length].value = top; + + break; + + case json_array: + + parent->u.array.values + [parent->u.array.length] = top; + + break; + + default: + break; + }; + } + + if ( (++ top->parent->u.array.length) > state.uint_max) + goto e_overflow; + + top = top->parent; + + continue; + } + } + + alloc = root; + } + + return root; + +e_unknown_value: + + sprintf (error, "%d:%d: Unknown value", cur_line, e_off); + goto e_failed; + +e_alloc_failure: + + strcpy (error, "Memory allocation failure"); + goto e_failed; + +e_overflow: + + sprintf (error, "%d:%d: Too long (caught overflow)", cur_line, e_off); + goto e_failed; + +e_failed: + + if (error_buf) + { + if (*error) + strcpy (error_buf, error); + else + strcpy (error_buf, "Unknown error"); + } + + if (state.first_pass) + alloc = root; + + while (alloc) + { + top = alloc->_reserved.next_alloc; + free (alloc); + alloc = top; + } + + if (!state.first_pass) + json_value_free (root); + + return 0; +} + +json_value * json_parse (const json_char * json) +{ + json_settings settings; + memset (&settings, 0, sizeof (json_settings)); + + return json_parse_ex (&settings, json, 0); +} + +void json_value_free (json_value * value) +{ + json_value * cur_value; + + if (!value) + return; + + value->parent = 0; + + while (value) + { + switch (value->type) + { + case json_array: + + if (!value->u.array.length) + { + free (value->u.array.values); + break; + } + + value = value->u.array.values [-- value->u.array.length]; + continue; + + case json_object: + + if (!value->u.object.length) + { + free (value->u.object.values); + break; + } + + value = value->u.object.values [-- value->u.object.length].value; + continue; + + case json_string: + + free (value->u.string.ptr); + break; + + default: + break; + }; + + cur_value = value; + value = value->parent; + free (cur_value); + } +} + + diff --git a/lib/json.h b/lib/json.h new file mode 100644 index 00000000..3800565a --- /dev/null +++ b/lib/json.h @@ -0,0 +1,192 @@ + +/* vim: set et ts=3 sw=3 ft=c: + * + * Copyright (C) 2012 James McLaughlin et al. All rights reserved. + * https://github.com/udp/json-parser + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef _JSON_H +#define _JSON_H + +#ifndef json_char + #define json_char char +#endif + +#ifdef __cplusplus + + #include <string.h> + + extern "C" + { + +#endif + +typedef struct +{ + unsigned long max_memory; + int settings; + +} json_settings; + +#define json_relaxed_commas 1 + +typedef enum +{ + json_none, + json_object, + json_array, + json_integer, + json_double, + json_string, + json_boolean, + json_null + +} json_type; + +extern const struct _json_value json_value_none; + +typedef struct _json_value +{ + struct _json_value * parent; + + json_type type; + + union + { + int boolean; + long long integer; + double dbl; + + struct + { + unsigned int length; + json_char * ptr; /* null terminated */ + + } string; + + struct + { + unsigned int length; + + struct + { + json_char * name; + struct _json_value * value; + + } * values; + + } object; + + struct + { + unsigned int length; + struct _json_value ** values; + + } array; + + } u; + + union + { + struct _json_value * next_alloc; + void * object_mem; + + } _reserved; + + + /* Some C++ operator sugar */ + + #ifdef __cplusplus + + public: + + inline _json_value () + { memset (this, 0, sizeof (_json_value)); + } + + inline const struct _json_value &operator [] (int index) const + { + if (type != json_array || index < 0 + || ((unsigned int) index) >= u.array.length) + { + return json_value_none; + } + + return *u.array.values [index]; + } + + inline const struct _json_value &operator [] (const char * index) const + { + if (type != json_object) + return json_value_none; + + for (unsigned int i = 0; i < u.object.length; ++ i) + if (!strcmp (u.object.values [i].name, index)) + return *u.object.values [i].value; + + return json_value_none; + } + + inline operator const char * () const + { + switch (type) + { + case json_string: + return u.string.ptr; + + default: + return ""; + }; + } + + inline operator long () const + { return u.integer; + } + + inline operator bool () const + { return u.boolean != 0; + } + + #endif + +} json_value; + +json_value * json_parse + (const json_char * json); + +json_value * json_parse_ex + (json_settings * settings, const json_char * json, char * error); + +void json_value_free (json_value *); + + +#ifdef __cplusplus + } /* extern "C" */ +#endif + +#endif + + diff --git a/lib/json_util.c b/lib/json_util.c new file mode 100644 index 00000000..941589ba --- /dev/null +++ b/lib/json_util.c @@ -0,0 +1,62 @@ +/***************************************************************************\ +* * +* BitlBee - An IRC to IM gateway * +* Helper functions for json.c * +* * +* Copyright 2012 Wilmer van der Gaast <wilmer@gaast.net> * +* * +* This library is free software; you can redistribute it and/or * +* modify it under the terms of the GNU Lesser General Public * +* License as published by the Free Software Foundation, version * +* 2.1. * +* * +* This library 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 * +* Lesser General Public License for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this library; if not, write to the Free Software Foundation, * +* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * +* * +****************************************************************************/ + +#include <stdlib.h> +#include <string.h> +#include <glib.h> + +#include "json_util.h" + +json_value *json_o_get( const json_value *obj, const json_char *name ) +{ + int i; + + if( !obj || obj->type != json_object ) + return NULL; + + for( i = 0; i < obj->u.object.length; ++ i) + if( strcmp( obj->u.object.values[i].name, name ) == 0 ) + return obj->u.object.values[i].value; + + return NULL; +} + +const char *json_o_str( const json_value *obj, const json_char *name ) +{ + json_value *ret = json_o_get( obj, name ); + + if( ret && ret->type == json_string ) + return ret->u.string.ptr; + else + return NULL; +} + +char *json_o_strdup( const json_value *obj, const json_char *name ) +{ + json_value *ret = json_o_get( obj, name ); + + if( ret && ret->type == json_string && ret->u.string.ptr ) + return g_memdup( ret->u.string.ptr, ret->u.string.length + 1 ); + else + return NULL; +} diff --git a/lib/json_util.h b/lib/json_util.h new file mode 100644 index 00000000..e0f1f7b8 --- /dev/null +++ b/lib/json_util.h @@ -0,0 +1,35 @@ +/***************************************************************************\ +* * +* BitlBee - An IRC to IM gateway * +* Helper functions for json.c * +* * +* Copyright 2012 Wilmer van der Gaast <wilmer@gaast.net> * +* * +* This library is free software; you can redistribute it and/or * +* modify it under the terms of the GNU Lesser General Public * +* License as published by the Free Software Foundation, version * +* 2.1. * +* * +* This library 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 * +* Lesser General Public License for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this library; if not, write to the Free Software Foundation, * +* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * +* * +****************************************************************************/ + +#include "json.h" + +#define JSON_O_FOREACH(o, k, v) \ + char *k; json_value *v; int __i; \ + for( __i = 0; ( __i < (o)->u.object.length ) && \ + ( k = (o)->u.object.values[__i].name ) && \ + ( v = (o)->u.object.values[__i].value ); \ + __i ++ ) + +json_value *json_o_get( const json_value *obj, const json_char *name ); +const char *json_o_str( const json_value *obj, const json_char *name ); +char *json_o_strdup( const json_value *obj, const json_char *name ); diff --git a/lib/oauth2.c b/lib/oauth2.c index d3f6bc09..6921a6d5 100644 --- a/lib/oauth2.c +++ b/lib/oauth2.c @@ -3,7 +3,7 @@ * BitlBee - An IRC to IM gateway * * Simple OAuth client (consumer) implementation. * * * -* Copyright 2010-2011 Wilmer van der Gaast <wilmer@gaast.net> * +* Copyright 2010-2012 Wilmer van der Gaast <wilmer@gaast.net> * * * * 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 * @@ -25,6 +25,7 @@ #include "http_client.h" #include "oauth2.h" #include "oauth.h" +#include "json.h" #include "url.h" char *oauth2_url( const struct oauth2_service *sp ) @@ -43,7 +44,6 @@ struct oauth2_access_token_data gpointer data; }; -static char *oauth2_json_dumb_get( const char *json, const char *key ); static void oauth2_access_token_done( struct http_request *req ); int oauth2_access_token( const struct oauth2_service *sp, @@ -114,8 +114,22 @@ static void oauth2_access_token_done( struct http_request *req ) } else if( content_type && strstr( content_type, "application/json" ) ) { - atoken = oauth2_json_dumb_get( req->reply_body, "access_token" ); - rtoken = oauth2_json_dumb_get( req->reply_body, "refresh_token" ); + json_value *js = json_parse( req->reply_body ); + if( js && js->type == json_object ) + { + int i; + + for( i = 0; i < js->u.object.length; i ++ ) + { + if( js->u.object.values[i].value->type != json_string ) + continue; + if( strcmp( js->u.object.values[i].name, "access_token" ) == 0 ) + atoken = g_strdup( js->u.object.values[i].value->u.string.ptr ); + if( strcmp( js->u.object.values[i].name, "refresh_token" ) == 0 ) + rtoken = g_strdup( js->u.object.values[i].value->u.string.ptr ); + } + } + json_value_free( js ); } else { @@ -136,62 +150,3 @@ static void oauth2_access_token_done( struct http_request *req ) g_free( rtoken ); g_free( cb_data ); } - -/* Super dumb. I absolutely refuse to use/add a complete json parser library - (adding a new dependency to BitlBee for the first time in.. 6 years?) just - to parse 100 bytes of data. So I have to do my own parsing because OAuth2 - dropped support for XML. (GRRR!) This is very dumb and for example won't - work for integer values, nor will it strip/handle backslashes. */ -static char *oauth2_json_dumb_get( const char *json, const char *key ) -{ - int is_key = 0; /* 1 == reading key, 0 == reading value */ - int found_key = 0; - - while( json && *json ) - { - /* Grab strings and see if they're what we're looking for. */ - if( *json == '"' || *json == '\'' ) - { - char q = *json; - const char *str_start; - json ++; - str_start = json; - - while( *json ) - { - /* \' and \" are not string terminators. */ - if( *json == '\\' && json[1] == q ) - json ++; - /* But without a \ it is. */ - else if( *json == q ) - break; - json ++; - } - if( *json == '\0' ) - return NULL; - - if( is_key && strncmp( str_start, key, strlen( key ) ) == 0 ) - { - found_key = 1; - } - else if( !is_key && found_key ) - { - char *ret = g_memdup( str_start, json - str_start + 1 ); - ret[json-str_start] = '\0'; - return ret; - } - - } - else if( *json == '{' || *json == ',' ) - { - found_key = 0; - is_key = 1; - } - else if( *json == ':' ) - is_key = 0; - - json ++; - } - - return NULL; -} diff --git a/lib/ssl_gnutls.c b/lib/ssl_gnutls.c index 41a76f09..45d24e6e 100644 --- a/lib/ssl_gnutls.c +++ b/lib/ssl_gnutls.c @@ -37,7 +37,7 @@ int ssl_errno = 0; static gboolean initialized = FALSE; -gnutls_certificate_credentials xcred; +gnutls_certificate_credentials_t xcred; #include <limits.h> @@ -59,7 +59,7 @@ struct scd char *hostname; gboolean verify; - gnutls_session session; + gnutls_session_t session; }; static gboolean ssl_connected( gpointer data, gint source, b_input_condition cond ); @@ -133,7 +133,7 @@ void *ssl_starttls( int fd, char *hostname, gboolean verify, ssl_input_function conn->func = func; conn->data = data; conn->inpa = -1; - conn->hostname = hostname; + conn->hostname = g_strdup( hostname ); /* For now, SSL verification is globally enabled by setting the cafile setting in bitlbee.conf. Commented out by default because probably @@ -170,9 +170,9 @@ static int verify_certificate_callback( gnutls_session_t session ) int gnutlsret; int verifyret = 0; gnutls_x509_crt_t cert; - const char *hostname; + struct scd *conn; - hostname = gnutls_session_get_ptr( session ); + conn = gnutls_session_get_ptr( session ); gnutlsret = gnutls_certificate_verify_peers2( session, &status ); if( gnutlsret < 0 ) @@ -210,7 +210,7 @@ static int verify_certificate_callback( gnutls_session_t session ) if( cert_list == NULL || gnutls_x509_crt_import( cert, &cert_list[0], GNUTLS_X509_FMT_DER ) < 0 ) return VERIFY_CERT_ERROR; - if( !gnutls_x509_crt_check_hostname( cert, hostname ) ) + if( !gnutls_x509_crt_check_hostname( cert, conn->hostname ) ) { verifyret |= VERIFY_CERT_INVALID; verifyret |= VERIFY_CERT_WRONG_HOSTNAME; @@ -266,8 +266,7 @@ static gboolean ssl_connected( gpointer data, gint source, b_input_condition con ssl_init(); gnutls_init( &conn->session, GNUTLS_CLIENT ); - if( conn->verify ) - gnutls_session_set_ptr( conn->session, (void *) conn->hostname ); + gnutls_session_set_ptr( conn->session, (void *) conn ); #if GNUTLS_VERSION_NUMBER < 0x020c00 gnutls_transport_set_lowat( conn->session, 0 ); #endif @@ -275,7 +274,7 @@ static gboolean ssl_connected( gpointer data, gint source, b_input_condition con gnutls_credentials_set( conn->session, GNUTLS_CRD_CERTIFICATE, xcred ); sock_make_nonblocking( conn->fd ); - gnutls_transport_set_ptr( conn->session, (gnutls_transport_ptr) GNUTLS_STUPID_CAST conn->fd ); + gnutls_transport_set_ptr( conn->session, (gnutls_transport_ptr_t) GNUTLS_STUPID_CAST conn->fd ); return ssl_handshake( data, source, cond ); } @@ -401,6 +400,7 @@ void ssl_disconnect( void *conn_ ) if( conn->session ) gnutls_deinit( conn->session ); + g_free( conn->hostname ); g_free( conn ); } @@ -133,7 +133,10 @@ char *nick_gen( bee_user_t *bu ) { chop = fmt[1]; if( chop == '\0' ) + { + g_string_free( ret, TRUE ); return NULL; + } fmt += 2; } else if( isdigit( *fmt ) ) @@ -186,6 +189,7 @@ char *nick_gen( bee_user_t *bu ) } else { + g_string_free( ret, TRUE ); return NULL; } } diff --git a/protocols/msn/msn.c b/protocols/msn/msn.c index 71570ce0..845f9cf8 100644 --- a/protocols/msn/msn.c +++ b/protocols/msn/msn.c @@ -52,6 +52,7 @@ static void msn_login( account_t *acc ) struct msn_data *md = g_new0( struct msn_data, 1 ); ic->proto_data = md; + ic->flags |= OPT_PONGS | OPT_PONGED; if( strchr( acc->user, '@' ) == NULL ) { diff --git a/protocols/msn/ns.c b/protocols/msn/ns.c index d9a558f9..7acf4654 100644 --- a/protocols/msn/ns.c +++ b/protocols/msn/ns.c @@ -576,6 +576,10 @@ static int msn_ns_command( struct msn_handler_data *handler, char **cmd, int num if( num_parts >= 7 ) handler->msglen = atoi( cmd[6] ); } + else if( strcmp( cmd[0], "QNG" ) == 0 ) + { + ic->flags |= OPT_PONGED; + } else if( isdigit( cmd[0][0] ) ) { int num = atoi( cmd[0] ); diff --git a/protocols/nogaim.c b/protocols/nogaim.c index 773e3877..2f85a3eb 100644 --- a/protocols/nogaim.c +++ b/protocols/nogaim.c @@ -260,12 +260,32 @@ static gboolean send_keepalive( gpointer d, gint fd, b_input_condition cond ) { struct im_connection *ic = d; + if( ( ic->flags & OPT_PONGS ) && !( ic->flags & OPT_PONGED ) ) + { + /* This protocol is expected to ack keepalives and hasn't + since the last time we were here. */ + imcb_error( ic, "Connection timeout" ); + imc_logout( ic, TRUE ); + return FALSE; + } + ic->flags &= ~OPT_PONGED; + if( ic->acc->prpl->keepalive ) ic->acc->prpl->keepalive( ic ); return TRUE; } +void start_keepalives( struct im_connection *ic, int interval ) +{ + b_event_remove( ic->keepalive ); + ic->keepalive = b_timeout_add( interval, send_keepalive, ic ); + + /* Connecting successfully counts as a first successful pong. */ + if( ic->flags & OPT_PONGS ) + ic->flags |= OPT_PONGED; +} + void imcb_connected( struct im_connection *ic ) { /* MSN servers sometimes redirect you to a different server and do @@ -276,9 +296,8 @@ void imcb_connected( struct im_connection *ic ) imcb_log( ic, "Logged in" ); - b_event_remove( ic->keepalive ); - ic->keepalive = b_timeout_add( 60000, send_keepalive, ic ); ic->flags |= OPT_LOGGED_IN; + start_keepalives( ic, 60000 ); /* Necessary to send initial presence status, even if we're not away. */ imc_away_send_update( ic ); diff --git a/protocols/nogaim.h b/protocols/nogaim.h index 0536ce69..aa6ba7c0 100644 --- a/protocols/nogaim.h +++ b/protocols/nogaim.h @@ -67,6 +67,8 @@ #define OPT_TYPING 0x00000100 /* Some pieces of code make assumptions */ #define OPT_THINKING 0x00000200 /* about these values... Stupid me! */ #define OPT_NOOTR 0x00001000 /* protocol not suitable for OTR */ +#define OPT_PONGS 0x00010000 /* Service sends us keep-alives */ +#define OPT_PONGED 0x00020000 /* Received a keep-alive during last interval */ /* ok. now the fun begins. first we create a connection structure */ struct im_connection diff --git a/protocols/twitter/twitter.c b/protocols/twitter/twitter.c index 93ef4ae2..651bf345 100644 --- a/protocols/twitter/twitter.c +++ b/protocols/twitter/twitter.c @@ -3,7 +3,8 @@ * BitlBee - An IRC to IM gateway * * Simple module to facilitate twitter functionality. * * * -* Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com> * +* Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com> * +* Copyright 2010-2012 Wilmer van der Gaast <wilmer@gaast.net> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * @@ -28,17 +29,7 @@ #include "twitter_lib.h" #include "url.h" -#define twitter_msg( ic, fmt... ) \ - do { \ - struct twitter_data *td = ic->proto_data; \ - if( td->timeline_gc ) \ - imcb_chat_log( td->timeline_gc, fmt ); \ - else \ - imcb_log( ic, fmt ); \ - } while( 0 ); - GSList *twitter_connections = NULL; - /** * Main loop function */ @@ -61,15 +52,57 @@ static void twitter_main_loop_start(struct im_connection *ic) { struct twitter_data *td = ic->proto_data; + /* Create the room now that we "logged in". */ + if (td->flags & TWITTER_MODE_CHAT) + twitter_groupchat_init(ic); + imcb_log(ic, "Getting initial statuses"); - // Run this once. After this queue the main loop function. + // Run this once. After this queue the main loop function (or open the + // stream if available). twitter_main_loop(ic, -1, 0); + + if (set_getbool(&ic->acc->set, "stream")) { + /* That fetch was just to get backlog, the stream will give + us the rest. \o/ */ + twitter_open_stream(ic); + + /* Stream sends keepalives (empty lines) or actual data at + least twice a minute. Disconnect if this stops. */ + ic->flags |= OPT_PONGS; + } else { + /* Not using the streaming API, so keep polling the old- + fashioned way. :-( */ + td->main_loop_id = + b_timeout_add(set_getint(&ic->acc->set, "fetch_interval") * 1000, + twitter_main_loop, ic); + } +} + +struct groupchat *twitter_groupchat_init(struct im_connection *ic) +{ + char *name_hint; + struct groupchat *gc; + struct twitter_data *td = ic->proto_data; + GSList *l; + + if (td->timeline_gc) + return td->timeline_gc; - // Queue the main_loop - // Save the return value, so we can remove the timeout on logout. - td->main_loop_id = - b_timeout_add(set_getint(&ic->acc->set, "fetch_interval") * 1000, twitter_main_loop, ic); + td->timeline_gc = gc = imcb_chat_new(ic, "twitter/timeline"); + + name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user); + imcb_chat_name_hint(gc, name_hint); + g_free(name_hint); + + for (l = ic->bee->users; l; l = l->next) { + bee_user_t *bu = l->data; + if (bu->ic == ic) + imcb_chat_add_buddy(gc, bu->handle); + } + imcb_chat_add_buddy(gc, ic->acc->user); + + return gc; } static void twitter_oauth_start(struct im_connection *ic); @@ -82,11 +115,10 @@ void twitter_login_finish(struct im_connection *ic) if (set_getbool(&ic->acc->set, "oauth") && !td->oauth_info) twitter_oauth_start(ic); - else if (g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "one") != 0 && - !(td->flags & TWITTER_HAVE_FRIENDS)) { + else if (!(td->flags & TWITTER_MODE_ONE) && + !(td->flags & TWITTER_HAVE_FRIENDS)) { imcb_log(ic, "Getting contact list"); twitter_get_friends_ids(ic, -1); - //twitter_get_statuses_friends(ic, -1); } else twitter_main_loop_start(ic); } @@ -186,16 +218,6 @@ static gboolean twitter_oauth_callback(struct oauth_info *info) return TRUE; } - -static char *set_eval_mode(set_t * set, char *value) -{ - if (g_strcasecmp(value, "one") == 0 || - g_strcasecmp(value, "many") == 0 || g_strcasecmp(value, "chat") == 0) - return value; - else - return NULL; -} - int twitter_url_len_diff(gchar *msg, unsigned int target_len) { int url_len_diff = 0; @@ -232,11 +254,28 @@ static gboolean twitter_length_check(struct im_connection *ic, gchar * msg) if (max == 0 || (len = g_utf8_strlen(msg, -1) + url_len_diff) <= max) return TRUE; - imcb_error(ic, "Maximum message length exceeded: %d > %d", len, max); + twitter_log(ic, "Maximum message length exceeded: %d > %d", len, max); return FALSE; } +static char *set_eval_commands(set_t * set, char *value) +{ + if (g_strcasecmp(value, "strict") == 0 ) + return value; + else + return set_eval_bool(set, value); +} + +static char *set_eval_mode(set_t * set, char *value) +{ + if (g_strcasecmp(value, "one") == 0 || + g_strcasecmp(value, "many") == 0 || g_strcasecmp(value, "chat") == 0) + return value; + else + return NULL; +} + static void twitter_init(account_t * acc) { set_t *s; @@ -256,7 +295,7 @@ static void twitter_init(account_t * acc) s = set_add(&acc->set, "base_url", def_url, NULL, acc); s->flags |= ACC_SET_OFFLINE_ONLY; - s = set_add(&acc->set, "commands", "true", set_eval_bool, acc); + s = set_add(&acc->set, "commands", "true", set_eval_commands, acc); s = set_add(&acc->set, "fetch_interval", "60", set_eval_int, acc); s->flags |= ACC_SET_OFFLINE_ONLY; @@ -273,15 +312,19 @@ static void twitter_init(account_t * acc) s = set_add(&acc->set, "oauth", "true", set_eval_oauth, acc); s = set_add(&acc->set, "show_ids", "true", set_eval_bool, acc); - s->flags |= ACC_SET_OFFLINE_ONLY; s = set_add(&acc->set, "show_old_mentions", "20", set_eval_int, acc); s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc); + + if (strcmp(acc->prpl->name, "twitter") == 0) { + s = set_add(&acc->set, "stream", "true", set_eval_bool, acc); + s->flags |= ACC_SET_OFFLINE_ONLY; + } } /** - * Login method. Since the twitter API works with seperate HTTP request we + * Login method. Since the twitter API works with separate HTTP request we * only save the user and pass to the twitter_data object. */ static void twitter_login(account_t * acc) @@ -299,6 +342,12 @@ static void twitter_login(account_t * acc) return; } + if (!strstr(url.host, "twitter.com") && + set_getbool(&ic->acc->set, "stream")) { + imcb_error(ic, "Warning: The streaming API is only supported by Twitter, " + "and you seem to be connecting to a different service."); + } + imcb_log(ic, "Connecting"); twitter_connections = g_slist_append(twitter_connections, ic); @@ -339,8 +388,16 @@ static void twitter_login(account_t * acc) imcb_add_buddy(ic, name, NULL); imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL); - if (set_getbool(&acc->set, "show_ids")) - td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH); + td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH); + td->log_id = -1; + + s = set_getstr(&ic->acc->set, "mode"); + if (g_strcasecmp(s, "one") == 0) + td->flags |= TWITTER_MODE_ONE; + else if (g_strcasecmp(s, "many") == 0) + td->flags |= TWITTER_MODE_MANY; + else + td->flags |= TWITTER_MODE_CHAT; twitter_login_finish(ic); } @@ -362,6 +419,7 @@ static void twitter_logout(struct im_connection *ic) imcb_chat_free(td->timeline_gc); if (td) { + http_close(td->stream); oauth_info_free(td->oauth_info); g_free(td->user); g_free(td->prefix); @@ -494,21 +552,40 @@ static void twitter_buddy_data_free(struct bee_user *bu) * * Returns 0 if the user provides garbage. */ -static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, struct twitter_data *td, char *arg) { +static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, char *arg, bee_user_t **bu_) { + struct twitter_data *td = ic->proto_data; struct twitter_user_data *tud; - bee_user_t *bu; + bee_user_t *bu = NULL; guint64 id = 0; - if (g_str_has_prefix(arg, "#") && - sscanf(arg + 1, "%" G_GUINT64_FORMAT, &id) == 1) { - if (id < TWITTER_LOG_LENGTH && td->log) - id = td->log[id].id; - } else if ((bu = bee_user_by_handle(ic->bee, ic, arg)) && - (tud = bu->data) && tud->last_id) - id = tud->last_id; - else if (sscanf(arg, "%" G_GUINT64_FORMAT, &id) == 1){ - if (id < TWITTER_LOG_LENGTH && td->log) + + if (bu_) + *bu_ = NULL; + if (!arg || !arg[0]) + return 0; + + if (arg[0] != '#' && (bu = bee_user_by_handle(ic->bee, ic, arg))) { + if ((tud = bu->data)) + id = tud->last_id; + } else { + if (arg[0] == '#') + arg++; + if (sscanf(arg, "%" G_GINT64_MODIFIER "x", &id) == 1 && + id < TWITTER_LOG_LENGTH) { + bu = td->log[id].bu; id = td->log[id].id; + /* Beware of dangling pointers! */ + if (!g_slist_find(ic->bee->users, bu)) + bu = NULL; + } else if (sscanf(arg, "%" G_GINT64_MODIFIER "d", &id) == 1) { + /* Allow normal tweet IDs as well; not a very useful + feature but it's always been there. Just ignore + very low IDs to avoid accidents. */ + if (id < 1000000) + id = 0; + } } + if (bu_) + *bu_ = bu; return id; } @@ -516,124 +593,85 @@ static void twitter_handle_command(struct im_connection *ic, char *message) { struct twitter_data *td = ic->proto_data; char *cmds, **cmd, *new = NULL; - guint64 in_reply_to = 0; + guint64 in_reply_to = 0, id; + gboolean allow_post = + g_strcasecmp(set_getstr(&ic->acc->set, "commands"), "strict") != 0; + bee_user_t *bu = NULL; cmds = g_strdup(message); cmd = split_command_parts(cmds); if (cmd[0] == NULL) { - g_free(cmds); - return; - } else if (!set_getbool(&ic->acc->set, "commands")) { - /* Not supporting commands. */ + goto eof; + } else if (!set_getbool(&ic->acc->set, "commands") && allow_post) { + /* Not supporting commands if "commands" is set to true/strict. */ } else if (g_strcasecmp(cmd[0], "undo") == 0) { - guint64 id; - if (cmd[1] == NULL) twitter_status_destroy(ic, td->last_status_id); - else if (sscanf(cmd[1], "%" G_GUINT64_FORMAT, &id) == 1) { - if (id < TWITTER_LOG_LENGTH && td->log) - id = td->log[id].id; - + else if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL))) twitter_status_destroy(ic, id); - } else - twitter_msg(ic, "Could not undo last action"); + else + twitter_log(ic, "Could not undo last action"); - g_free(cmds); - return; + goto eof; } else if (g_strcasecmp(cmd[0], "favourite") == 0 && cmd[1]) { - guint64 id; - if ((id = twitter_message_id_from_command_arg(ic, td, cmd[1]))) { + if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL))) { twitter_favourite_tweet(ic, id); } else { - twitter_msg(ic, "Please provide a message ID or username."); + twitter_log(ic, "Please provide a message ID or username."); } - g_free(cmds); - return; + goto eof; } else if (g_strcasecmp(cmd[0], "follow") == 0 && cmd[1]) { twitter_add_buddy(ic, cmd[1], NULL); - g_free(cmds); - return; + goto eof; } else if (g_strcasecmp(cmd[0], "unfollow") == 0 && cmd[1]) { twitter_remove_buddy(ic, cmd[1], NULL); - g_free(cmds); - return; + goto eof; } else if ((g_strcasecmp(cmd[0], "report") == 0 || g_strcasecmp(cmd[0], "spam") == 0) && cmd[1]) { - char * screen_name; - guint64 id; - screen_name = cmd[1]; + char *screen_name; + /* Report nominally works on users but look up the user who posted the given ID if the user wants to do it that way */ - if (g_str_has_prefix(cmd[1], "#") && - sscanf(cmd[1] + 1, "%" G_GUINT64_FORMAT, &id) == 1) { - if (id < TWITTER_LOG_LENGTH && td->log) { - if (g_slist_find(ic->bee->users, td->log[id].bu)) { - screen_name = td->log[id].bu->handle; - } - } - } + twitter_message_id_from_command_arg(ic, cmd[1], &bu); + if (bu) + screen_name = bu->handle; + else + screen_name = cmd[1]; + twitter_report_spam(ic, screen_name); - g_free(cmds); - return; + goto eof; } else if (g_strcasecmp(cmd[0], "rt") == 0 && cmd[1]) { - guint64 id = twitter_message_id_from_command_arg(ic, td, cmd[1]); + id = twitter_message_id_from_command_arg(ic, cmd[1], NULL); td->last_status_id = 0; if (id) twitter_status_retweet(ic, id); else - twitter_msg(ic, "User `%s' does not exist or didn't " + twitter_log(ic, "User `%s' does not exist or didn't " "post any statuses recently", cmd[1]); - g_free(cmds); - return; + goto eof; } else if (g_strcasecmp(cmd[0], "reply") == 0 && cmd[1] && cmd[2]) { - struct twitter_user_data *tud; - bee_user_t *bu = NULL; - guint64 id = 0; - - if (g_str_has_prefix(cmd[1], "#") && - sscanf(cmd[1] + 1, "%" G_GUINT64_FORMAT, &id) == 1 && - (id < TWITTER_LOG_LENGTH) && td->log) { - bu = td->log[id].bu; - if (g_slist_find(ic->bee->users, bu)) - id = td->log[id].id; - else - bu = NULL; - } else if ((bu = bee_user_by_handle(ic->bee, ic, cmd[1])) && - (tud = bu->data) && tud->last_id) { - id = tud->last_id; - } else if (sscanf(cmd[1], "%" G_GUINT64_FORMAT, &id) == 1 && - (id < TWITTER_LOG_LENGTH) && td->log) { - bu = td->log[id].bu; - if (g_slist_find(ic->bee->users, bu)) - id = td->log[id].id; - else - bu = NULL; - } - + id = twitter_message_id_from_command_arg(ic, cmd[1], &bu); if (!id || !bu) { - twitter_msg(ic, "User `%s' does not exist or didn't " + twitter_log(ic, "User `%s' does not exist or didn't " "post any statuses recently", cmd[1]); - g_free(cmds); - return; + goto eof; } message = new = g_strdup_printf("@%s %s", bu->handle, message + (cmd[2] - cmd[0])); in_reply_to = id; + allow_post = TRUE; } else if (g_strcasecmp(cmd[0], "post") == 0) { message += 5; + allow_post = TRUE; } - { + if (allow_post) { char *s; - bee_user_t *bu; - if (!twitter_length_check(ic, message)) { - g_free(new); - g_free(cmds); - return; - } + if (!twitter_length_check(ic, message)) + goto eof; s = cmd[0] + strlen(cmd[0]) - 1; if (!new && s > cmd[0] && (*s == ':' || *s == ',')) { @@ -656,11 +694,33 @@ static void twitter_handle_command(struct im_connection *ic, char *message) this would delete the second-last Tweet. Prevent that. */ td->last_status_id = 0; twitter_post_status(ic, message, in_reply_to); - g_free(new); + } else { + twitter_log(ic, "Unknown command: %s", cmd[0]); } +eof: + g_free(new); g_free(cmds); } +void twitter_log(struct im_connection *ic, char *format, ... ) +{ + struct twitter_data *td = ic->proto_data; + va_list params; + char *text; + + va_start(params, format); + text = g_strdup_vprintf(format, params); + va_end(params); + + if (td->timeline_gc) + imcb_chat_log(td->timeline_gc, "%s", text); + else + imcb_log(ic, "%s", text); + + g_free(text); +} + + void twitter_initmodule() { struct prpl *ret = g_new0(struct prpl, 1); diff --git a/protocols/twitter/twitter.h b/protocols/twitter/twitter.h index 14e43824..8792b7c9 100644 --- a/protocols/twitter/twitter.h +++ b/protocols/twitter/twitter.h @@ -3,7 +3,8 @@ * BitlBee - An IRC to IM gateway * * Simple module to facilitate twitter functionality. * * * -* Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com> * +* Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com> * +* Copyright 2010-2012 Wilmer van der Gaast <wilmer@gaast.net> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * @@ -34,10 +35,13 @@ typedef enum { - TWITTER_HAVE_FRIENDS = 1, + TWITTER_HAVE_FRIENDS = 0x00001, + TWITTER_MODE_ONE = 0x00002, + TWITTER_MODE_MANY = 0x00004, + TWITTER_MODE_CHAT = 0x00008, TWITTER_DOING_TIMELINE = 0x10000, - TWITTER_GOT_TIMELINE = 0x20000, - TWITTER_GOT_MENTIONS = 0x40000, + TWITTER_GOT_TIMELINE = 0x20000, + TWITTER_GOT_MENTIONS = 0x40000, } twitter_flags_t; struct twitter_log_data; @@ -56,6 +60,7 @@ struct twitter_data guint64 last_status_id; /* For undo */ gint main_loop_id; + struct http_request *stream; struct groupchat *timeline_gc; gint http_fails; twitter_flags_t flags; @@ -79,7 +84,7 @@ struct twitter_user_data time_t last_time; }; -#define TWITTER_LOG_LENGTH 100 +#define TWITTER_LOG_LENGTH 256 struct twitter_log_data { guint64 id; @@ -98,4 +103,7 @@ void twitter_login_finish( struct im_connection *ic ); struct http_request; char *twitter_parse_error( struct http_request *req ); +void twitter_log(struct im_connection *ic, char *format, ... ); +struct groupchat *twitter_groupchat_init(struct im_connection *ic); + #endif //_TWITTER_H diff --git a/protocols/twitter/twitter_http.c b/protocols/twitter/twitter_http.c index dbac5461..0f1ab518 100644 --- a/protocols/twitter/twitter_http.c +++ b/protocols/twitter/twitter_http.c @@ -46,14 +46,15 @@ static char *twitter_url_append(char *url, char *key, char *value); * Do a request. * This is actually pretty generic function... Perhaps it should move to the lib/http_client.c */ -void *twitter_http(struct im_connection *ic, char *url_string, http_input_function func, - gpointer data, int is_post, char **arguments, int arguments_len) +struct http_request *twitter_http(struct im_connection *ic, char *url_string, http_input_function func, + gpointer data, int is_post, char **arguments, int arguments_len) { struct twitter_data *td = ic->proto_data; char *tmp; GString *request = g_string_new(""); void *ret; char *url_arguments; + url_t *base_url = NULL; url_arguments = g_strdup(""); @@ -66,20 +67,34 @@ void *twitter_http(struct im_connection *ic, char *url_string, http_input_functi url_arguments = tmp; } } + + if (strstr(url_string, "://")) { + base_url = g_new0(url_t, 1); + if (!url_set(base_url, url_string)) { + g_free(base_url); + return NULL; + } + } + // Make the request. g_string_printf(request, "%s %s%s%s%s HTTP/1.0\r\n" "Host: %s\r\n" "User-Agent: BitlBee " BITLBEE_VERSION " " ARCH "/" CPU "\r\n", is_post ? "POST" : "GET", - td->url_path, url_string, - is_post ? "" : "?", is_post ? "" : url_arguments, td->url_host); + base_url ? base_url->file : td->url_path, + base_url ? "" : url_string, + is_post ? "" : "?", is_post ? "" : url_arguments, + base_url ? base_url->host : td->url_host); // If a pass and user are given we append them to the request. if (td->oauth_info) { char *full_header; char *full_url; - full_url = g_strconcat(set_getstr(&ic->acc->set, "base_url"), url_string, NULL); + if (base_url) + full_url = g_strdup(url_string); + else + full_url = g_strconcat(set_getstr(&ic->acc->set, "base_url"), url_string, NULL); full_header = oauth_http_header(td->oauth_info, is_post ? "POST" : "GET", full_url, url_arguments); @@ -108,10 +123,23 @@ void *twitter_http(struct im_connection *ic, char *url_string, http_input_functi g_string_append(request, "\r\n"); } - ret = http_dorequest(td->url_host, td->url_port, td->url_ssl, request->str, func, data); + if (base_url) + ret = http_dorequest(base_url->host, base_url->port, base_url->proto == PROTO_HTTPS, request->str, func, data); + else + ret = http_dorequest(td->url_host, td->url_port, td->url_ssl, request->str, func, data); g_free(url_arguments); g_string_free(request, TRUE); + g_free(base_url); + return ret; +} + +struct http_request *twitter_http_f(struct im_connection *ic, char *url_string, http_input_function func, + gpointer data, int is_post, char **arguments, int arguments_len, twitter_http_flags_t flags) +{ + struct http_request *ret = twitter_http(ic, url_string, func, data, is_post, arguments, arguments_len); + if (ret) + ret->flags |= flags; return ret; } diff --git a/protocols/twitter/twitter_http.h b/protocols/twitter/twitter_http.h index 393a1c26..09ef350c 100644 --- a/protocols/twitter/twitter_http.h +++ b/protocols/twitter/twitter_http.h @@ -27,10 +27,18 @@ #include "nogaim.h" #include "http_client.h" +typedef enum { + /* With this set, twitter_http_post() will post a generic confirmation + message to the user. */ + TWITTER_HTTP_USER_ACK = 0x1000000, +} twitter_http_flags_t; + struct oauth_info; -void *twitter_http(struct im_connection *ic, char *url_string, http_input_function func, - gpointer data, int is_post, char** arguments, int arguments_len); +struct http_request *twitter_http(struct im_connection *ic, char *url_string, http_input_function func, + gpointer data, int is_post, char** arguments, int arguments_len); +struct http_request *twitter_http_f(struct im_connection *ic, char *url_string, http_input_function func, + gpointer data, int is_post, char** arguments, int arguments_len, twitter_http_flags_t flags); #endif //_TWITTER_HTTP_H diff --git a/protocols/twitter/twitter_lib.c b/protocols/twitter/twitter_lib.c index 4a09cbb1..3d368d2e 100644 --- a/protocols/twitter/twitter_lib.c +++ b/protocols/twitter/twitter_lib.c @@ -34,8 +34,8 @@ #include "url.h" #include "misc.h" #include "base64.h" -#include "xmltree.h" #include "twitter_lib.h" +#include "json_util.h" #include <ctype.h> #include <errno.h> @@ -66,11 +66,10 @@ struct twitter_xml_status { time_t created_at; char *text; struct twitter_xml_user *user; - guint64 id, reply_to; + guint64 id, rt_id; /* Usually equal, with RTs id == *original* id */ + guint64 reply_to; }; -static void twitter_groupchat_init(struct im_connection *ic); - /** * Frees a twitter_xml_user struct. */ @@ -147,17 +146,16 @@ static void twitter_add_buddy(struct im_connection *ic, char *name, const char * // Check if the buddy is already in the buddy list. if (!bee_user_by_handle(ic->bee, ic, name)) { - char *mode = set_getstr(&ic->acc->set, "mode"); - // The buddy is not in the list, add the buddy and set the status to logged in. imcb_add_buddy(ic, name, NULL); imcb_rename_buddy(ic, name, fullname); - if (g_strcasecmp(mode, "chat") == 0) { + if (td->flags & TWITTER_MODE_CHAT) { /* Necessary so that nicks always get translated to the exact Twitter username. */ imcb_buddy_nick_hint(ic, name, name); - imcb_chat_add_buddy(td->timeline_gc, name); - } else if (g_strcasecmp(mode, "many") == 0) + if (td->timeline_gc) + imcb_chat_add_buddy(td->timeline_gc, name); + } else if (td->flags & TWITTER_MODE_MANY) imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL); } } @@ -167,32 +165,34 @@ static void twitter_add_buddy(struct im_connection *ic, char *name, const char * char *twitter_parse_error(struct http_request *req) { static char *ret = NULL; - struct xt_node *root, *node, *err; + json_value *root, *err; g_free(ret); ret = NULL; if (req->body_size > 0) { - root = xt_from_string(req->reply_body, req->body_size); - - for (node = root; node; node = node->next) - if ((err = xt_find_node(node->children, "error")) && err->text_len > 0) { - ret = g_strdup_printf("%s (%s)", req->status_string, err->text); - break; - } - - xt_free_node(root); + root = json_parse(req->reply_body); + err = json_o_get(root, "errors"); + if (err && err->type == json_array && (err = err->u.array.values[0]) && + err->type == json_object) { + const char *msg = json_o_str(err, "message"); + if (msg) + ret = g_strdup_printf("%s (%s)", req->status_string, msg); + } + json_value_free(root); } return ret ? ret : req->status_string; } -static struct xt_node *twitter_parse_response(struct im_connection *ic, struct http_request *req) +/* WATCH OUT: This function might or might not destroy your connection. + Sub-optimal indeed, but just be careful when this returns NULL! */ +static json_value *twitter_parse_response(struct im_connection *ic, struct http_request *req) { gboolean logging_in = !(ic->flags & OPT_LOGGED_IN); gboolean periodic; struct twitter_data *td = ic->proto_data; - struct xt_node *ret; + json_value *ret; char path[64] = "", *s; if ((s = strchr(req->request, ' '))) { @@ -210,14 +210,15 @@ static struct xt_node *twitter_parse_response(struct im_connection *ic, struct h /* IIRC Twitter once had an outage where they were randomly throwing 401s so I'll keep treating this one as fatal only during login. */ - imcb_error(ic, "Authentication failure"); + imcb_error(ic, "Authentication failure (%s)", + twitter_parse_error(req)); imc_logout(ic, FALSE); return NULL; } else if (req->status_code != 200) { // It didn't go well, output the error and return. if (!periodic || logging_in || ++td->http_fails >= 5) - imcb_error(ic, "Could not retrieve %s: %s", - path, twitter_parse_error(req)); + twitter_log(ic, "Error: Could not retrieve %s: %s", + path, twitter_parse_error(req)); if (logging_in) imc_logout(ic, TRUE); @@ -226,7 +227,7 @@ static struct xt_node *twitter_parse_response(struct im_connection *ic, struct h td->http_fails = 0; } - if ((ret = xt_from_string(req->reply_body, req->body_size)) == NULL) { + if ((ret = json_parse(req->reply_body)) == NULL) { imcb_error(ic, "Could not retrieve %s: %s", path, "XML parse error"); } @@ -250,45 +251,35 @@ void twitter_get_friends_ids(struct im_connection *ic, gint64 next_cursor) } /** - * Function to help fill a list. - */ -static xt_status twitter_xt_next_cursor(struct xt_node *node, struct twitter_xml_list *txl) -{ - char *end = NULL; - - if (node->text) - txl->next_cursor = g_ascii_strtoll(node->text, &end, 10); - if (end == NULL) - txl->next_cursor = -1; - - return XT_HANDLED; -} - -/** * Fill a list of ids. */ -static xt_status twitter_xt_get_friends_id_list(struct xt_node *node, struct twitter_xml_list *txl) +static gboolean twitter_xt_get_friends_id_list(json_value *node, struct twitter_xml_list *txl) { - struct xt_node *child; + json_value *c; + int i; // Set the list type. txl->type = TXL_ID; - // The root <statuses> node should hold the list of statuses <status> - // Walk over the nodes children. - for (child = node->children; child; child = child->next) { - if (g_strcasecmp("ids", child->name) == 0) { - struct xt_node *idc; - for (idc = child->children; idc; idc = idc->next) - if (g_strcasecmp(idc->name, "id") == 0) - txl->list = g_slist_prepend(txl->list, - g_memdup(idc->text, idc->text_len + 1)); - } else if (g_strcasecmp("next_cursor", child->name) == 0) { - twitter_xt_next_cursor(child, txl); - } - } + c = json_o_get(node, "ids"); + if (!c || c->type != json_array) + return FALSE; - return XT_HANDLED; + for (i = 0; i < c->u.array.length; i ++) { + if (c->u.array.values[i]->type != json_integer) + continue; + + txl->list = g_slist_prepend(txl->list, + g_strdup_printf("%lld", c->u.array.values[i]->u.integer)); + } + + c = json_o_get(node, "next_cursor"); + if (c && c->type == json_integer) + txl->next_cursor = c->u.integer; + else + txl->next_cursor = -1; + + return TRUE; } static void twitter_get_users_lookup(struct im_connection *ic); @@ -299,7 +290,7 @@ static void twitter_get_users_lookup(struct im_connection *ic); static void twitter_http_get_friends_ids(struct http_request *req) { struct im_connection *ic; - struct xt_node *parsed; + json_value *parsed; struct twitter_xml_list *txl; struct twitter_data *td; @@ -311,18 +302,15 @@ static void twitter_http_get_friends_ids(struct http_request *req) td = ic->proto_data; - /* Create the room now that we "logged in". */ - if (!td->timeline_gc && g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "chat") == 0) - twitter_groupchat_init(ic); - txl = g_new0(struct twitter_xml_list, 1); txl->list = td->follow_ids; // Parse the data. if (!(parsed = twitter_parse_response(ic, req))) return; + twitter_xt_get_friends_id_list(parsed, txl); - xt_free_node(parsed); + json_value_free(parsed); td->follow_ids = txl->list; if (txl->next_cursor) @@ -337,7 +325,7 @@ static void twitter_http_get_friends_ids(struct http_request *req) txl_free(txl); } -static xt_status twitter_xt_get_users(struct xt_node *node, struct twitter_xml_list *txl); +static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl); static void twitter_http_get_users_lookup(struct http_request *req); static void twitter_get_users_lookup(struct im_connection *ic) @@ -378,7 +366,7 @@ static void twitter_get_users_lookup(struct im_connection *ic) static void twitter_http_get_users_lookup(struct http_request *req) { struct im_connection *ic = req->data; - struct xt_node *parsed; + json_value *parsed; struct twitter_xml_list *txl; GSList *l = NULL; struct twitter_xml_user *user; @@ -394,7 +382,7 @@ static void twitter_http_get_users_lookup(struct http_request *req) if (!(parsed = twitter_parse_response(ic, req))) return; twitter_xt_get_users(parsed, txl); - xt_free_node(parsed); + json_value_free(parsed); // Add the users as buddies. for (l = txl->list; l; l = g_slist_next(l)) { @@ -408,25 +396,15 @@ static void twitter_http_get_users_lookup(struct http_request *req) twitter_get_users_lookup(ic); } -/** - * Function to fill a twitter_xml_user struct. - * It sets: - * - the name and - * - the screen_name. - */ -static xt_status twitter_xt_get_user(struct xt_node *node, struct twitter_xml_user *txu) +struct twitter_xml_user *twitter_xt_get_user(const json_value *node) { - struct xt_node *child; - - // Walk over the nodes children. - for (child = node->children; child; child = child->next) { - if (g_strcasecmp("name", child->name) == 0) { - txu->name = g_memdup(child->text, child->text_len + 1); - } else if (g_strcasecmp("screen_name", child->name) == 0) { - txu->screen_name = g_memdup(child->text, child->text_len + 1); - } - } - return XT_HANDLED; + struct twitter_xml_user *txu; + + txu = g_new0(struct twitter_xml_user, 1); + txu->name = g_strdup(json_o_str(node, "name")); + txu->screen_name = g_strdup(json_o_str(node, "screen_name")); + + return txu; } /** @@ -434,26 +412,26 @@ static xt_status twitter_xt_get_user(struct xt_node *node, struct twitter_xml_us * It sets: * - all <user>s from the <users> element. */ -static xt_status twitter_xt_get_users(struct xt_node *node, struct twitter_xml_list *txl) +static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl) { struct twitter_xml_user *txu; - struct xt_node *child; + int i; // Set the type of the list. txl->type = TXL_USER; + if (!node || node->type != json_array) + return FALSE; + // The root <users> node should hold the list of users <user> // Walk over the nodes children. - for (child = node->children; child; child = child->next) { - if (g_strcasecmp("user", child->name) == 0) { - txu = g_new0(struct twitter_xml_user, 1); - twitter_xt_get_user(child, txu); - // Put the item in the front of the list. + for (i = 0; i < node->u.array.length; i ++) { + txu = twitter_xt_get_user(node->u.array.values[i]); + if (txu) txl->list = g_slist_prepend(txl->list, txu); - } } - return XT_HANDLED; + return TRUE; } #ifdef __GLIBC__ @@ -462,6 +440,8 @@ static xt_status twitter_xt_get_users(struct xt_node *node, struct twitter_xml_l #define TWITTER_TIME_FORMAT "%a %b %d %H:%M:%S +0000 %Y" #endif +static char* expand_entities(char* text, const json_value *entities); + /** * Function to fill a twitter_xml_status struct. * It sets: @@ -470,77 +450,133 @@ static xt_status twitter_xt_get_users(struct xt_node *node, struct twitter_xml_l * - the status id and * - the user in a twitter_xml_user struct. */ -static xt_status twitter_xt_get_status(struct xt_node *node, struct twitter_xml_status *txs) +static struct twitter_xml_status *twitter_xt_get_status(const json_value *node) { - struct xt_node *child, *rt = NULL; - - // Walk over the nodes children. - for (child = node->children; child; child = child->next) { - if (g_strcasecmp("text", child->name) == 0) { - txs->text = g_memdup(child->text, child->text_len + 1); - } else if (g_strcasecmp("retweeted_status", child->name) == 0) { - rt = child; - } else if (g_strcasecmp("created_at", child->name) == 0) { + struct twitter_xml_status *txs; + const json_value *rt = NULL, *entities = NULL; + + if (node->type != json_object) + return FALSE; + txs = g_new0(struct twitter_xml_status, 1); + + JSON_O_FOREACH (node, k, v) { + if (strcmp("text", k) == 0 && v->type == json_string) { + txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1); + strip_html(txs->text); + } else if (strcmp("retweeted_status", k) == 0 && v->type == json_object) { + rt = v; + } else if (strcmp("created_at", k) == 0 && v->type == json_string) { struct tm parsed; /* Very sensitive to changes to the formatting of this field. :-( Also assumes the timezone used is UTC since C time handling functions suck. */ - if (strptime(child->text, TWITTER_TIME_FORMAT, &parsed) != NULL) + if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL) txs->created_at = mktime_utc(&parsed); - } else if (g_strcasecmp("user", child->name) == 0) { - txs->user = g_new0(struct twitter_xml_user, 1); - twitter_xt_get_user(child, txs->user); - } else if (g_strcasecmp("id", child->name) == 0) { - txs->id = g_ascii_strtoull(child->text, NULL, 10); - } else if (g_strcasecmp("in_reply_to_status_id", child->name) == 0) { - txs->reply_to = g_ascii_strtoull(child->text, NULL, 10); + } else if (strcmp("user", k) == 0 && v->type == json_object) { + txs->user = twitter_xt_get_user(v); + } else if (strcmp("id", k) == 0 && v->type == json_integer) { + txs->rt_id = txs->id = v->u.integer; + } else if (strcmp("in_reply_to_status_id", k) == 0 && v->type == json_integer) { + txs->reply_to = v->u.integer; + } else if (strcmp("entities", k) == 0 && v->type == json_object) { + entities = v; } } /* If it's a (truncated) retweet, get the original. Even if the API claims it wasn't truncated because it may be lying. */ if (rt) { - struct twitter_xml_status *rtxs = g_new0(struct twitter_xml_status, 1); - if (twitter_xt_get_status(rt, rtxs) != XT_HANDLED) { + struct twitter_xml_status *rtxs = twitter_xt_get_status(rt); + if (rtxs) { + g_free(txs->text); + txs->text = g_strdup_printf("RT @%s: %s", rtxs->user->screen_name, rtxs->text); + txs->id = rtxs->id; txs_free(rtxs); - return XT_HANDLED; } + } else if (entities) { + txs->text = expand_entities(txs->text, entities); + } - g_free(txs->text); - txs->text = g_strdup_printf("RT @%s: %s", rtxs->user->screen_name, rtxs->text); - txs_free(rtxs); - } else { - struct xt_node *urls, *url; + if (txs->text && txs->user && txs->id) + return txs; + + txs_free(txs); + return NULL; +} + +/** + * Function to fill a twitter_xml_status struct (DM variant). + */ +static struct twitter_xml_status *twitter_xt_get_dm(const json_value *node) +{ + struct twitter_xml_status *txs; + const json_value *entities = NULL; + + if (node->type != json_object) + return FALSE; + txs = g_new0(struct twitter_xml_status, 1); + + JSON_O_FOREACH (node, k, v) { + if (strcmp("text", k) == 0 && v->type == json_string) { + txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1); + strip_html(txs->text); + } else if (strcmp("created_at", k) == 0 && v->type == json_string) { + struct tm parsed; + + /* Very sensitive to changes to the formatting of + this field. :-( Also assumes the timezone used + is UTC since C time handling functions suck. */ + if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL) + txs->created_at = mktime_utc(&parsed); + } else if (strcmp("sender", k) == 0 && v->type == json_object) { + txs->user = twitter_xt_get_user(v); + } else if (strcmp("id", k) == 0 && v->type == json_integer) { + txs->id = v->u.integer; + } + } + + if (entities) { + txs->text = expand_entities(txs->text, entities); + } + + if (txs->text && txs->user && txs->id) + return txs; + + txs_free(txs); + return NULL; +} + +static char* expand_entities(char* text, const json_value *entities) { + JSON_O_FOREACH (entities, k, v) { + int i; + + if (v->type != json_array) + continue; + if (strcmp(k, "urls") != 0 && strcmp(k, "media") != 0) + continue; - urls = xt_find_path(node, "entities"); - if (urls != NULL) - urls = urls->children; - for (; urls; urls = urls->next) { - if (strcmp(urls->name, "urls") != 0 && strcmp(urls->name, "media") != 0) + for (i = 0; i < v->u.array.length; i ++) { + if (v->u.array.values[i]->type != json_object) continue; - for (url = urls ? urls->children : NULL; url; url = url->next) { - /* "short" is a reserved word. :-P */ - struct xt_node *kort = xt_find_node(url->children, "url"); - struct xt_node *disp = xt_find_node(url->children, "display_url"); - char *pos, *new; - - if (!kort || !kort->text || !disp || !disp->text || - !(pos = strstr(txs->text, kort->text))) - continue; - - *pos = '\0'; - new = g_strdup_printf("%s%s <%s>%s", txs->text, kort->text, - disp->text, pos + strlen(kort->text)); - - g_free(txs->text); - txs->text = new; - } + const char *kort = json_o_str(v->u.array.values[i], "url"); + const char *disp = json_o_str(v->u.array.values[i], "display_url"); + char *pos, *new; + + if (!kort || !disp || !(pos = strstr(text, kort))) + continue; + + *pos = '\0'; + new = g_strdup_printf("%s%s <%s>%s", text, kort, + disp, pos + strlen(kort)); + + g_free(text); + text = new; } } - - return XT_HANDLED; + + return text; } /** @@ -549,198 +585,332 @@ static xt_status twitter_xt_get_status(struct xt_node *node, struct twitter_xml_ * - all <status>es within the <status> element and * - the next_cursor. */ -static xt_status twitter_xt_get_status_list(struct im_connection *ic, struct xt_node *node, - struct twitter_xml_list *txl) +static gboolean twitter_xt_get_status_list(struct im_connection *ic, const json_value *node, + struct twitter_xml_list *txl) { struct twitter_xml_status *txs; - struct xt_node *child; - bee_user_t *bu; + int i; // Set the type of the list. txl->type = TXL_STATUS; + + if (node->type != json_array) + return FALSE; // The root <statuses> node should hold the list of statuses <status> // Walk over the nodes children. - for (child = node->children; child; child = child->next) { - if (g_strcasecmp("status", child->name) == 0) { - txs = g_new0(struct twitter_xml_status, 1); - twitter_xt_get_status(child, txs); - // Put the item in the front of the list. - txl->list = g_slist_prepend(txl->list, txs); - - if (txs->user && txs->user->screen_name && - (bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) { - struct twitter_user_data *tud = bu->data; - - if (txs->id > tud->last_id) { - tud->last_id = txs->id; - tud->last_time = txs->created_at; - } - } - } else if (g_strcasecmp("next_cursor", child->name) == 0) { - twitter_xt_next_cursor(child, txl); - } + for (i = 0; i < node->u.array.length; i ++) { + txs = twitter_xt_get_status(node->u.array.values[i]); + if (!txs) + continue; + + txl->list = g_slist_prepend(txl->list, txs); } - return XT_HANDLED; + return TRUE; } +/* Will log messages either way. Need to keep track of IDs for stream deduping. + Plus, show_ids is on by default and I don't see why anyone would disable it. */ static char *twitter_msg_add_id(struct im_connection *ic, struct twitter_xml_status *txs, const char *prefix) { struct twitter_data *td = ic->proto_data; - char *ret = NULL; - - if (!set_getbool(&ic->acc->set, "show_ids")) { - if (*prefix) - return g_strconcat(prefix, txs->text, NULL); - else - return NULL; - } + int reply_to = -1; + bee_user_t *bu; - td->log[td->log_id].id = txs->id; - td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name); if (txs->reply_to) { int i; for (i = 0; i < TWITTER_LOG_LENGTH; i++) if (td->log[i].id == txs->reply_to) { - ret = g_strdup_printf("\002[\002%02d->%02d\002]\002 %s%s", - td->log_id, i, prefix, txs->text); + reply_to = i; break; } } - if (ret == NULL) - ret = g_strdup_printf("\002[\002%02d\002]\002 %s%s", td->log_id, prefix, txs->text); - td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH; - return ret; -} + if (txs->user && txs->user->screen_name && + (bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) { + struct twitter_user_data *tud = bu->data; -static void twitter_groupchat_init(struct im_connection *ic) -{ - char *name_hint; - struct groupchat *gc; - struct twitter_data *td = ic->proto_data; - GSList *l; - - td->timeline_gc = gc = imcb_chat_new(ic, "twitter/timeline"); - - name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user); - imcb_chat_name_hint(gc, name_hint); - g_free(name_hint); - - for (l = ic->bee->users; l; l = l->next) { - bee_user_t *bu = l->data; - if (bu->ic == ic) - imcb_chat_add_buddy(td->timeline_gc, bu->handle); + if (txs->id > tud->last_id) { + tud->last_id = txs->id; + tud->last_time = txs->created_at; + } + } + + td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH; + td->log[td->log_id].id = txs->id; + td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name); + + /* This is all getting hairy. :-( If we RT'ed something ourselves, + remember OUR id instead so undo will work. In other cases, the + original tweet's id should be remembered for deduplicating. */ + if (strcmp(txs->user->screen_name, td->user) == 0) + td->log[td->log_id].id = txs->rt_id; + + if (set_getbool(&ic->acc->set, "show_ids")) { + if (reply_to != -1) + return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s", + td->log_id, reply_to, prefix, txs->text); + else + return g_strdup_printf("\002[\002%02x\002]\002 %s%s", + td->log_id, prefix, txs->text); + } else { + if (*prefix) + return g_strconcat(prefix, txs->text, NULL); + else + return NULL; } } /** * Function that is called to see the statuses in a groupchat window. */ -static void twitter_groupchat(struct im_connection *ic, GSList * list) +static void twitter_status_show_chat(struct im_connection *ic, struct twitter_xml_status *status) { struct twitter_data *td = ic->proto_data; - GSList *l = NULL; - struct twitter_xml_status *status; struct groupchat *gc; - guint64 last_id = 0; + gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0; + char *msg; // Create a new groupchat if it does not exsist. - if (!td->timeline_gc) - twitter_groupchat_init(ic); - - gc = td->timeline_gc; - if (!gc->joined) - imcb_chat_add_buddy(gc, ic->acc->user); - - for (l = list; l; l = g_slist_next(l)) { - char *msg; - - status = l->data; - if (status->user == NULL || status->text == NULL || last_id == status->id) - continue; - - last_id = status->id; - - strip_html(status->text); - - if (set_getbool(&ic->acc->set, "strip_newlines")) - strip_newlines(status->text); - - msg = twitter_msg_add_id(ic, status, ""); - - // Say it! - if (g_strcasecmp(td->user, status->user->screen_name) == 0) { - imcb_chat_log(gc, "You: %s", msg ? msg : status->text); - } else { - twitter_add_buddy(ic, status->user->screen_name, status->user->name); + gc = twitter_groupchat_init(ic); - imcb_chat_msg(gc, status->user->screen_name, - msg ? msg : status->text, 0, status->created_at); - } - - g_free(msg); - - // Update the timeline_id to hold the highest id, so that by the next request - // we won't pick up the updates already in the list. - td->timeline_id = MAX(td->timeline_id, status->id); + if (!me) + /* MUST be done before twitter_msg_add_id() to avoid #872. */ + twitter_add_buddy(ic, status->user->screen_name, status->user->name); + msg = twitter_msg_add_id(ic, status, ""); + + // Say it! + if (me) { + imcb_chat_log(gc, "You: %s", msg ? msg : status->text); + } else { + imcb_chat_msg(gc, status->user->screen_name, + msg ? msg : status->text, 0, status->created_at); } + + g_free(msg); } /** * Function that is called to see statuses as private messages. */ -static void twitter_private_message_chat(struct im_connection *ic, GSList * list) +static void twitter_status_show_msg(struct im_connection *ic, struct twitter_xml_status *status) { struct twitter_data *td = ic->proto_data; - GSList *l = NULL; - struct twitter_xml_status *status; - char from[MAX_STRING]; - gboolean mode_one; - guint64 last_id = 0; + char from[MAX_STRING] = ""; + char *prefix = NULL, *text = NULL; + gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0; - mode_one = g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "one") == 0; - - if (mode_one) { + if (td->flags & TWITTER_MODE_ONE) { g_snprintf(from, sizeof(from) - 1, "%s_%s", td->prefix, ic->acc->user); from[MAX_STRING - 1] = '\0'; } - for (l = list; l; l = g_slist_next(l)) { - char *prefix = NULL, *text = NULL; + if (td->flags & TWITTER_MODE_ONE) + prefix = g_strdup_printf("\002<\002%s\002>\002 ", + status->user->screen_name); + else if (!me) + twitter_add_buddy(ic, status->user->screen_name, status->user->name); + else + prefix = g_strdup("You: "); - status = l->data; - if (status->user == NULL || status->text == NULL || last_id == status->id) - continue; + text = twitter_msg_add_id(ic, status, prefix ? prefix : ""); - last_id = status->id; + imcb_buddy_msg(ic, + *from ? from : status->user->screen_name, + text ? text : status->text, 0, status->created_at); - strip_html(status->text); - if (mode_one) - prefix = g_strdup_printf("\002<\002%s\002>\002 ", - status->user->screen_name); - else - twitter_add_buddy(ic, status->user->screen_name, status->user->name); + g_free(text); + g_free(prefix); +} - text = twitter_msg_add_id(ic, status, prefix ? prefix : ""); +static void twitter_status_show(struct im_connection *ic, struct twitter_xml_status *status) +{ + struct twitter_data *td = ic->proto_data; + + if (status->user == NULL || status->text == NULL) + return; + + /* Grrrr. Would like to do this during parsing, but can't access + settings from there. */ + if (set_getbool(&ic->acc->set, "strip_newlines")) + strip_newlines(status->text); + + if (td->flags & TWITTER_MODE_CHAT) + twitter_status_show_chat(ic, status); + else + twitter_status_show_msg(ic, status); - imcb_buddy_msg(ic, - mode_one ? from : status->user->screen_name, - text ? text : status->text, 0, status->created_at); + // Update the timeline_id to hold the highest id, so that by the next request + // we won't pick up the updates already in the list. + td->timeline_id = MAX(td->timeline_id, status->rt_id); +} - // Update the timeline_id to hold the highest id, so that by the next request - // we won't pick up the updates already in the list. - td->timeline_id = MAX(td->timeline_id, status->id); +static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o); - g_free(text); - g_free(prefix); +static void twitter_http_stream(struct http_request *req) +{ + struct im_connection *ic = req->data; + struct twitter_data *td; + json_value *parsed; + int len = 0; + char c, *nl; + + if (!g_slist_find(twitter_connections, ic)) + return; + + ic->flags |= OPT_PONGED; + td = ic->proto_data; + + if ((req->flags & HTTPC_EOF) || !req->reply_body) { + td->stream = NULL; + imcb_error(ic, "Stream closed (%s)", req->status_string); + imc_logout(ic, TRUE); + return; + } + + printf( "%d bytes in stream\n", req->body_size ); + + /* MUST search for CRLF, not just LF: + https://dev.twitter.com/docs/streaming-apis/processing#Parsing_responses */ + nl = strstr(req->reply_body, "\r\n"); + + if (!nl) { + printf("Incomplete data\n"); + return; + } + + len = nl - req->reply_body; + if (len > 0) { + c = req->reply_body[len]; + req->reply_body[len] = '\0'; + + printf("JSON: %s\n", req->reply_body); + printf("parsed: %p\n", (parsed = json_parse(req->reply_body))); + if (parsed) { + twitter_stream_handle_object(ic, parsed); + } + json_value_free(parsed); + req->reply_body[len] = c; } + + http_flush_bytes(req, len + 2); + + /* One notification might bring multiple events! */ + if (req->body_size > 0) + twitter_http_stream(req); } -static void twitter_http_get_home_timeline(struct http_request *req); -static void twitter_http_get_mentions(struct http_request *req); +static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o); +static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs); + +static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o) +{ + struct twitter_data *td = ic->proto_data; + struct twitter_xml_status *txs; + json_value *c; + + if ((txs = twitter_xt_get_status(o))) { + gboolean ret = twitter_stream_handle_status(ic, txs); + txs_free(txs); + return ret; + } else if ((c = json_o_get(o, "direct_message")) && + (txs = twitter_xt_get_dm(c))) { + if (strcmp(txs->user->screen_name, td->user) != 0) + imcb_buddy_msg(ic, txs->user->screen_name, + txs->text, 0, txs->created_at); + txs_free(txs); + return TRUE; + } else if ((c = json_o_get(o, "event")) && c->type == json_string) { + twitter_stream_handle_event(ic, o); + return TRUE; + } else if ((c = json_o_get(o, "disconnect")) && c->type == json_object) { + /* HACK: Because we're inside an event handler, we can't just + disconnect here. Instead, just change the HTTP status string + into a Twitter status string. */ + char *reason = json_o_strdup(c, "reason"); + if (reason) { + g_free(td->stream->status_string); + td->stream->status_string = reason; + } + return TRUE; + } + return FALSE; +} + +static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs) +{ + struct twitter_data *td = ic->proto_data; + int i; + + for (i = 0; i < TWITTER_LOG_LENGTH; i++) { + if (td->log[i].id == txs->id) { + /* Got a duplicate (RT, probably). Drop it. */ + return TRUE; + } + } + + if (!(strcmp(txs->user->screen_name, td->user) == 0 || + set_getbool(&ic->acc->set, "fetch_mentions") || + bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) { + /* Tweet is from an unknown person and the user does not want + to see @mentions, so drop it. twitter_stream_handle_event() + picks up new follows so this simple filter should be safe. */ + /* TODO: The streaming API seems to do poor @mention matching. + I.e. I'm getting mentions for @WilmerSomething, not just for + @Wilmer. But meh. You want spam, you get spam. */ + return TRUE; + } + + twitter_status_show(ic, txs); + + return TRUE; +} + +static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o) +{ + struct twitter_data *td = ic->proto_data; + json_value *source = json_o_get(o, "source"); + json_value *target = json_o_get(o, "target"); + const char *type = json_o_str(o, "event"); + + if (!type || !source || source->type != json_object + || !target || target->type != json_object) { + return FALSE; + } + + if (strcmp(type, "follow") == 0) { + struct twitter_xml_user *us = twitter_xt_get_user(source); + struct twitter_xml_user *ut = twitter_xt_get_user(target); + if (strcmp(us->screen_name, td->user) == 0) { + twitter_add_buddy(ic, ut->screen_name, ut->name); + } + txu_free(us); + txu_free(ut); + } + + return TRUE; +} + +gboolean twitter_open_stream(struct im_connection *ic) +{ + struct twitter_data *td = ic->proto_data; + char *args[2] = {"with", "followings"}; + + if ((td->stream = twitter_http(ic, TWITTER_USER_STREAM_URL, + twitter_http_stream, ic, 0, args, 2))) { + /* This flag must be enabled or we'll get no data until EOF + (which err, kind of, defeats the purpose of a streaming API). */ + td->stream->flags |= HTTPC_STREAMING; + return TRUE; + } + + return FALSE; +} + +static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor); +static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor); /** * Get the timeline with optionally mentions @@ -777,9 +947,12 @@ void twitter_flush_timeline(struct im_connection *ic) int show_old_mentions = set_getint(&ic->acc->set, "show_old_mentions"); struct twitter_xml_list *home_timeline = td->home_timeline_obj; struct twitter_xml_list *mentions = td->mentions_obj; + guint64 last_id = 0; GSList *output = NULL; GSList *l; + imcb_connected(ic); + if (!(td->flags & TWITTER_GOT_TIMELINE)) { return; } @@ -803,17 +976,15 @@ void twitter_flush_timeline(struct im_connection *ic) output = g_slist_insert_sorted(output, l->data, twitter_compare_elements); } } - - if (!(ic->flags & OPT_LOGGED_IN)) - imcb_connected(ic); // See if the user wants to see the messages in a groupchat window or as private messages. - if (g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "chat") == 0) - twitter_groupchat(ic, output); - else - twitter_private_message_chat(ic, output); - - g_slist_free(output); + while (output) { + struct twitter_xml_status *txs = output->data; + if (txs->id != last_id) + twitter_status_show(ic, txs); + last_id = txs->id; + output = g_slist_remove(output, txs); + } txl_free(home_timeline); txl_free(mentions); @@ -822,10 +993,13 @@ void twitter_flush_timeline(struct im_connection *ic) td->home_timeline_obj = td->mentions_obj = NULL; } +static void twitter_http_get_home_timeline(struct http_request *req); +static void twitter_http_get_mentions(struct http_request *req); + /** * Get the timeline. */ -void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor) +static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor) { struct twitter_data *td = ic->proto_data; @@ -861,7 +1035,7 @@ void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor) /** * Get mentions. */ -void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor) +static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor) { struct twitter_data *td = ic->proto_data; @@ -892,9 +1066,7 @@ void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor) } g_free(args[1]); - if (td->timeline_id) { - g_free(args[5]); - } + g_free(args[5]); } /** @@ -904,7 +1076,7 @@ static void twitter_http_get_home_timeline(struct http_request *req) { struct im_connection *ic = req->data; struct twitter_data *td; - struct xt_node *parsed; + json_value *parsed; struct twitter_xml_list *txl; // Check if the connection is still active. @@ -920,11 +1092,14 @@ static void twitter_http_get_home_timeline(struct http_request *req) if (!(parsed = twitter_parse_response(ic, req))) goto end; twitter_xt_get_status_list(ic, parsed, txl); - xt_free_node(parsed); + json_value_free(parsed); td->home_timeline_obj = txl; end: + if (!g_slist_find(twitter_connections, ic)) + return; + td->flags |= TWITTER_GOT_TIMELINE; twitter_flush_timeline(ic); @@ -937,7 +1112,7 @@ static void twitter_http_get_mentions(struct http_request *req) { struct im_connection *ic = req->data; struct twitter_data *td; - struct xt_node *parsed; + json_value *parsed; struct twitter_xml_list *txl; // Check if the connection is still active. @@ -953,11 +1128,14 @@ static void twitter_http_get_mentions(struct http_request *req) if (!(parsed = twitter_parse_response(ic, req))) goto end; twitter_xt_get_status_list(ic, parsed, txl); - xt_free_node(parsed); + json_value_free(parsed); td->mentions_obj = txl; end: + if (!g_slist_find(twitter_connections, ic)) + return; + td->flags |= TWITTER_GOT_MENTIONS; twitter_flush_timeline(ic); @@ -971,7 +1149,7 @@ static void twitter_http_post(struct http_request *req) { struct im_connection *ic = req->data; struct twitter_data *td; - struct xt_node *parsed, *node; + json_value *parsed, *id; // Check if the connection is still active. if (!g_slist_find(twitter_connections, ic)) @@ -983,9 +1161,14 @@ static void twitter_http_post(struct http_request *req) if (!(parsed = twitter_parse_response(ic, req))) return; - if ((node = xt_find_node(parsed, "status")) && - (node = xt_find_node(node->children, "id")) && node->text) - td->last_status_id = g_ascii_strtoull(node->text, NULL, 10); + if ((id = json_o_get(parsed, "id")) && id->type == json_integer) { + td->last_status_id = id->u.integer; + } + + json_value_free(parsed); + + if (req->flags & TWITTER_HTTP_USER_ACK) + twitter_log(ic, "Command processed successfully"); } /** @@ -1031,8 +1214,9 @@ void twitter_status_destroy(struct im_connection *ic, guint64 id) { char *url; url = g_strdup_printf("%s%llu%s", TWITTER_STATUS_DESTROY_URL, - (unsigned long long) id, ".xml"); - twitter_http(ic, url, twitter_http_post, ic, 1, NULL, 0); + (unsigned long long) id, ".json"); + twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0, + TWITTER_HTTP_USER_ACK); g_free(url); } @@ -1040,8 +1224,9 @@ void twitter_status_retweet(struct im_connection *ic, guint64 id) { char *url; url = g_strdup_printf("%s%llu%s", TWITTER_STATUS_RETWEET_URL, - (unsigned long long) id, ".xml"); - twitter_http(ic, url, twitter_http_post, ic, 1, NULL, 0); + (unsigned long long) id, ".json"); + twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0, + TWITTER_HTTP_USER_ACK); g_free(url); } @@ -1055,8 +1240,8 @@ void twitter_report_spam(struct im_connection *ic, char *screen_name) NULL, }; args[1] = screen_name; - twitter_http(ic, TWITTER_REPORT_SPAM_URL, twitter_http_post, - ic, 1, args, 2); + twitter_http_f(ic, TWITTER_REPORT_SPAM_URL, twitter_http_post, + ic, 1, args, 2, TWITTER_HTTP_USER_ACK); } /** @@ -1066,7 +1251,8 @@ void twitter_favourite_tweet(struct im_connection *ic, guint64 id) { char *url; url = g_strdup_printf("%s%llu%s", TWITTER_FAVORITE_CREATE_URL, - (unsigned long long) id, ".xml"); - twitter_http(ic, url, twitter_http_post, ic, 1, NULL, 0); + (unsigned long long) id, ".json"); + twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0, + TWITTER_HTTP_USER_ACK); g_free(url); } diff --git a/protocols/twitter/twitter_lib.h b/protocols/twitter/twitter_lib.h index 2404e4eb..d5f5b16a 100644 --- a/protocols/twitter/twitter_lib.h +++ b/protocols/twitter/twitter_lib.h @@ -28,46 +28,46 @@ #include "nogaim.h" #include "twitter_http.h" -#define TWITTER_API_URL "http://api.twitter.com/1" +#define TWITTER_API_URL "http://api.twitter.com/1.1" #define IDENTICA_API_URL "https://identi.ca/api" /* Status URLs */ -#define TWITTER_STATUS_UPDATE_URL "/statuses/update.xml" +#define TWITTER_STATUS_UPDATE_URL "/statuses/update.json" #define TWITTER_STATUS_SHOW_URL "/statuses/show/" #define TWITTER_STATUS_DESTROY_URL "/statuses/destroy/" #define TWITTER_STATUS_RETWEET_URL "/statuses/retweet/" /* Timeline URLs */ -#define TWITTER_PUBLIC_TIMELINE_URL "/statuses/public_timeline.xml" -#define TWITTER_FEATURED_USERS_URL "/statuses/featured.xml" -#define TWITTER_FRIENDS_TIMELINE_URL "/statuses/friends_timeline.xml" -#define TWITTER_HOME_TIMELINE_URL "/statuses/home_timeline.xml" -#define TWITTER_MENTIONS_URL "/statuses/mentions.xml" -#define TWITTER_USER_TIMELINE_URL "/statuses/user_timeline.xml" +#define TWITTER_PUBLIC_TIMELINE_URL "/statuses/public_timeline.json" +#define TWITTER_FEATURED_USERS_URL "/statuses/featured.json" +#define TWITTER_FRIENDS_TIMELINE_URL "/statuses/friends_timeline.json" +#define TWITTER_HOME_TIMELINE_URL "/statuses/home_timeline.json" +#define TWITTER_MENTIONS_URL "/statuses/mentions_timeline.json" +#define TWITTER_USER_TIMELINE_URL "/statuses/user_timeline.json" /* Users URLs */ -#define TWITTER_USERS_LOOKUP_URL "/users/lookup.xml" +#define TWITTER_USERS_LOOKUP_URL "/users/lookup.json" /* Direct messages URLs */ -#define TWITTER_DIRECT_MESSAGES_URL "/direct_messages.xml" -#define TWITTER_DIRECT_MESSAGES_NEW_URL "/direct_messages/new.xml" -#define TWITTER_DIRECT_MESSAGES_SENT_URL "/direct_messages/sent.xml" +#define TWITTER_DIRECT_MESSAGES_URL "/direct_messages.json" +#define TWITTER_DIRECT_MESSAGES_NEW_URL "/direct_messages/new.json" +#define TWITTER_DIRECT_MESSAGES_SENT_URL "/direct_messages/sent.json" #define TWITTER_DIRECT_MESSAGES_DESTROY_URL "/direct_messages/destroy/" /* Friendships URLs */ -#define TWITTER_FRIENDSHIPS_CREATE_URL "/friendships/create.xml" -#define TWITTER_FRIENDSHIPS_DESTROY_URL "/friendships/destroy.xml" -#define TWITTER_FRIENDSHIPS_SHOW_URL "/friendships/show.xml" +#define TWITTER_FRIENDSHIPS_CREATE_URL "/friendships/create.json" +#define TWITTER_FRIENDSHIPS_DESTROY_URL "/friendships/destroy.json" +#define TWITTER_FRIENDSHIPS_SHOW_URL "/friendships/show.json" /* Social graphs URLs */ -#define TWITTER_FRIENDS_IDS_URL "/friends/ids.xml" -#define TWITTER_FOLLOWERS_IDS_URL "/followers/ids.xml" +#define TWITTER_FRIENDS_IDS_URL "/friends/ids.json" +#define TWITTER_FOLLOWERS_IDS_URL "/followers/ids.json" /* Account URLs */ -#define TWITTER_ACCOUNT_RATE_LIMIT_URL "/account/rate_limit_status.xml" +#define TWITTER_ACCOUNT_RATE_LIMIT_URL "/account/rate_limit_status.json" /* Favorites URLs */ -#define TWITTER_FAVORITES_GET_URL "/favorites.xml" +#define TWITTER_FAVORITES_GET_URL "/favorites.json" #define TWITTER_FAVORITE_CREATE_URL "/favorites/create/" #define TWITTER_FAVORITE_DESTROY_URL "/favorites/destroy/" @@ -76,12 +76,13 @@ #define TWITTER_BLOCKS_DESTROY_URL "/blocks/destroy/" /* Report spam */ -#define TWITTER_REPORT_SPAM_URL "/report_spam.xml" +#define TWITTER_REPORT_SPAM_URL "/report_spam.json" +#define TWITTER_USER_STREAM_URL "https://userstream.twitter.com/1.1/user.json" + +gboolean twitter_open_stream(struct im_connection *ic); void twitter_get_timeline(struct im_connection *ic, gint64 next_cursor); void twitter_get_friends_ids(struct im_connection *ic, gint64 next_cursor); -void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor); -void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor); void twitter_get_statuses_friends(struct im_connection *ic, gint64 next_cursor); void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to); |