From c9ec0c206544b37139057932a6dceed88565a75f Mon Sep 17 00:00:00 2001 From: Topi Reinio Date: Fri, 21 Jun 2013 10:50:48 +0200 Subject: [PATCH] Doc: Update Tweet Search Demo to use Twitter Search API v1.1 Twitter REST API v1 is no longer supported. This change updates the Tweet Search Demo to use the new version (v1.1). Specifically, - Use of OAuth tokens (authentication required in v1.1) - JSON parsing for results instead of XML - Use of url/hashtag/username entities returned in search results Also, update the documentation to discuss authentication and registering the application to dev.twitter.com. Task-number: QTBUG-31745 Change-Id: I00cd7b07f065babb03483daabe8df22f22995c29 Reviewed-by: Alan Alpert --- .../demos/tweetsearch/content/TweetDelegate.qml | 6 +- .../demos/tweetsearch/content/TweetsModel.qml | 96 +++++++++++++++------- .../quick/demos/tweetsearch/content/tweetsearch.js | 61 ++++++++++++-- .../demos/tweetsearch/doc/src/tweetsearch.qdoc | 45 +++++++++- examples/quick/demos/tweetsearch/tweetsearch.pro | 5 ++ examples/quick/demos/tweetsearch/tweetsearch.qml | 20 +++-- 6 files changed, 180 insertions(+), 53 deletions(-) diff --git a/examples/quick/demos/tweetsearch/content/TweetDelegate.qml b/examples/quick/demos/tweetsearch/content/TweetDelegate.qml index 8cd2211..e8bfff4 100644 --- a/examples/quick/demos/tweetsearch/content/TweetDelegate.qml +++ b/examples/quick/demos/tweetsearch/content/TweetDelegate.qml @@ -103,7 +103,7 @@ Item { Text { id: name - text: Helper.realName(model.name) + text: model.name anchors { left: avatar.right; leftMargin: 10; top: avatar.top; topMargin: -3 } font.pixelSize: 12 font.bold: true @@ -121,7 +121,7 @@ Item { color: "#adebff" linkColor: "white" onLinkActivated: { - var tag = link.split("http://search.twitter.com/search?q=%23") + var tag = link.split("https://twitter.com/search?q=%23") var user = link.split("https://twitter.com/") if (tag[1] != undefined) { mainListView.positionViewAtBeginning() @@ -166,7 +166,7 @@ Item { Text { id: username - text: Helper.twitterName(model.name) + text: model.twitterName x: 10; anchors { top: avatar2.top; topMargin: -3 } font.pixelSize: 12 font.bold: true diff --git a/examples/quick/demos/tweetsearch/content/TweetsModel.qml b/examples/quick/demos/tweetsearch/content/TweetsModel.qml index cd91a78..7d813d1 100644 --- a/examples/quick/demos/tweetsearch/content/TweetsModel.qml +++ b/examples/quick/demos/tweetsearch/content/TweetsModel.qml @@ -39,53 +39,87 @@ ****************************************************************************/ import QtQuick 2.0 -import QtQuick.XmlListModel 2.0 +import "tweetsearch.js" as Helper Item { id: wrapper - property variant model: xmlModel + // Insert valid consumer key and secret tokens below + // See https://dev.twitter.com/apps +//! [auth tokens] + property string consumerKey : "" + property string consumerSecret : "" +//! [auth tokens] + property string bearerToken : "" + + property variant model: tweets property string from : "" property string phrase : "" - property string mode : "everyone" - property int status: xmlModel.status - - function reload() { xmlModel.reload(); } - - property bool isLoading: status == XmlListModel.Loading + property int status: XMLHttpRequest.UNSENT + property bool isLoading: status === XMLHttpRequest.LOADING property bool wasLoading: false signal isLoaded - XmlListModel { - id: xmlModel + ListModel { id: tweets } - onStatusChanged: { - if (status == XmlListModel.Ready && wasLoading == true) - wrapper.isLoaded() - if (status == XmlListModel.Loading) - wasLoading = true; - else - wasLoading = false; - } + function encodePhrase(x) { return encodeURIComponent(x); } - function encodePhrase(x) { return encodeURIComponent(x); } + function reload() { + tweets.clear() - source: (from == "" && phrase == "") ? "" : - 'http://search.twitter.com/search.atom?from='+from+"&rpp=10&phrase="+encodePhrase(phrase) + if (from == "" && phrase == "") + return; - namespaceDeclarations: "declare default element namespace 'http://www.w3.org/2005/Atom'; " + - "declare namespace twitter=\"http://api.twitter.com/\";"; +//! [requesting] + var req = new XMLHttpRequest; + req.open("GET", "https://api.twitter.com/1.1/search/tweets.json?from=" + from + + "&count=10&q=" + encodePhrase(phrase)); + req.setRequestHeader("Authorization", "Bearer " + bearerToken); + req.onreadystatechange = function() { + status = req.readyState; + if (status === XMLHttpRequest.DONE) { + var objectArray = JSON.parse(req.responseText); + if (objectArray.errors !== undefined) + console.log("Error fetching tweets: " + objectArray.errors[0].message) + else { + for (var key in objectArray.statuses) { + var jsonObject = objectArray.statuses[key]; + tweets.append(jsonObject); + } + } + if (wasLoading == true) + wrapper.isLoaded() + } + wasLoading = (status === XMLHttpRequest.LOADING); + } + req.send(); +//! [requesting] + } - query: "/feed/entry" + onPhraseChanged: reload(); + onFromChanged: reload(); - XmlRole { name: "id"; query: "id/string()" } - XmlRole { name: "content"; query: "content/string()" } - XmlRole { name: "published"; query: "published/string()" } - XmlRole { name: "source"; query: "twitter:source/string()" } - XmlRole { name: "name"; query: "author/name/string()" } - XmlRole { name: "uri"; query: "author/uri/string()" } - XmlRole { name: "image"; query: "link[@rel = 'image']/@href/string()" } + Component.onCompleted: { + if (consumerKey === "" || consumerSecret == "") { + bearerToken = encodeURIComponent(Helper.demoToken()) + return; + } + var authReq = new XMLHttpRequest; + authReq.open("POST", "https://api.twitter.com/oauth2/token"); + authReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); + authReq.setRequestHeader("Authorization", "Basic " + Qt.btoa(consumerKey + ":" + consumerSecret)); + authReq.onreadystatechange = function() { + if (authReq.readyState === XMLHttpRequest.DONE) { + var jsonResponse = JSON.parse(authReq.responseText); + if (jsonResponse.errors !== undefined) + console.log("Authentication error: " + jsonResponse.errors[0].message) + else + bearerToken = jsonResponse.access_token; + } + } + authReq.send("grant_type=client_credentials"); } + } diff --git a/examples/quick/demos/tweetsearch/content/tweetsearch.js b/examples/quick/demos/tweetsearch/content/tweetsearch.js index 9b8638f..42a76c9 100644 --- a/examples/quick/demos/tweetsearch/content/tweetsearch.js +++ b/examples/quick/demos/tweetsearch/content/tweetsearch.js @@ -1,19 +1,62 @@ .pragma library -function twitterName(str) +function formatDate(date) { - var s = str.split("(") - return s[0] + var da = new Date(date) + return da.toDateString() } -function realName(str) +function demoToken() { - var s = str.split("(") - return s[1].substring(0, s[1].length-1) + var a = new Array(22).join('A') + return a + String.fromCharCode(0x44, 0x69, 0x4a, 0x52, 0x51, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x74, 0x2b, 0x72, 0x6a, 0x6c, 0x2b, 0x71, + 0x6d, 0x7a, 0x30, 0x72, 0x63, 0x79, 0x2b, 0x42, 0x62, + 0x75, 0x58, 0x42, 0x42, 0x73, 0x72, 0x55, 0x48, 0x47, + 0x45, 0x67, 0x3d, 0x71, 0x30, 0x45, 0x4b, 0x32, 0x61, + 0x57, 0x71, 0x51, 0x4d, 0x62, 0x31, 0x35, 0x67, 0x43, + 0x5a, 0x4e, 0x77, 0x5a, 0x6f, 0x39, 0x79, 0x71, 0x61, + 0x65, 0x30, 0x68, 0x70, 0x65, 0x32, 0x46, 0x44, 0x73, + 0x53, 0x39, 0x32, 0x57, 0x41, 0x75, 0x30, 0x67) } -function formatDate(date) +function linkForEntity(entity) { - var da = new Date(date) - return da.toDateString() + return (entity.url ? entity.url : + (entity.screen_name ? 'https://twitter.com/' + entity.screen_name : + 'https://twitter.com/search?q=%23' + entity.text)) +} + +function textForEntity(entity) +{ + return (entity.display_url ? entity.display_url : + (entity.screen_name ? entity.screen_name : entity.text)) +} + +function insertLinks(text, entities) +{ + if (typeof text !== 'string') + return ""; + + if (!entities) + return text; + + // Add all links (urls, usernames and hashtags) to an array and sort them in + // descending order of appearance in text + var links = [] + if (entities.urls) + links = entities.urls.concat(entities.hashtags, entities.user_mentions) + else if (entities.url) + links = entities.url.urls + + links.sort(function(a, b) { return b.indices[0] - a.indices[0] }) + + for (var i = 0; i < links.length; i++) { + var offset = links[i].url ? 0 : 1 + text = text.substring(0, links[i].indices[0] + offset) + + '' + + textForEntity(links[i]) + '' + + text.substring(links[i].indices[1]) + } + return text.replace(/\n/g, '
'); } diff --git a/examples/quick/demos/tweetsearch/doc/src/tweetsearch.qdoc b/examples/quick/demos/tweetsearch/doc/src/tweetsearch.qdoc index 9ba252f..a56ed0d 100644 --- a/examples/quick/demos/tweetsearch/doc/src/tweetsearch.qdoc +++ b/examples/quick/demos/tweetsearch/doc/src/tweetsearch.qdoc @@ -32,5 +32,48 @@ \brief A Twitter search client with 3D effects. \image qtquick-demo-tweetsearch-med-1.png \image qtquick-demo-tweetsearch-med-2.png -*/ + \section1 Demo Introduction + + The Tweet Search demo searches items posted to Twitter service + using a number of query parameters. Search can be done for tweets + from a specified user, a hashtag or a search phrase. + + The search result is a list of items showing the contents of the + tweet as well as the name and image of the user who posted it. + Hashtags, names and links in the content are clickable. Clicking + on the image will flip the item to reveal more information. + + \section1 Running the Demo + + Tweet Search uses Twitter API v1.1 for running seaches. + + \section2 Authentication + + Each request must be authenticated on behalf of the application. + For demonstration purposes, the application uses a hard-coded + token for identifying itself to the Twitter service. However, this + token is subject to rate limits for the number of requests as well + as possible expiration. + + If you are having authentication or rate limit problems running the + demo, obtain a set of application-specific tokens (consumer + key and consumer secret) by registering a new application on + \l{https://dev.twitter.com/apps}. + + Type in the two token values in \e {TweetsModel.qml}: + + \snippet demos/tweetsearch/content/TweetsModel.qml auth tokens + + Rebuild and run the demo. + + \section2 JSON Parsing + + Search results are returned in JSON (JavaScript Object Notation) + format. \c TweetsModel uses an \l XMLHTTPRequest object to send + an HTTP GET request, and calls JSON.parse() on the returned text + string to convert it to a JavaScript object. Each object + representing a tweet is then added to a \l ListModel: + + \snippet demos/tweetsearch/content/TweetsModel.qml requesting +*/ diff --git a/examples/quick/demos/tweetsearch/tweetsearch.pro b/examples/quick/demos/tweetsearch/tweetsearch.pro index b063cc4..27c34ba 100644 --- a/examples/quick/demos/tweetsearch/tweetsearch.pro +++ b/examples/quick/demos/tweetsearch/tweetsearch.pro @@ -4,5 +4,10 @@ QT += quick qml SOURCES += main.cpp RESOURCES += tweetsearch.qrc +OTHER_FILES = tweetsearch.qml \ + content/*.qml \ + content/*.js \ + content/resources/* + target.path = $$[QT_INSTALL_EXAMPLES]/quick/demos/tweetsearch INSTALLS += target diff --git a/examples/quick/demos/tweetsearch/tweetsearch.qml b/examples/quick/demos/tweetsearch/tweetsearch.qml index d7e77ce..19d3b5e 100644 --- a/examples/quick/demos/tweetsearch/tweetsearch.qml +++ b/examples/quick/demos/tweetsearch/tweetsearch.qml @@ -40,6 +40,7 @@ import QtQuick 2.0 import "content" +import "content/tweetsearch.js" as Helper Rectangle { id: main @@ -47,7 +48,6 @@ Rectangle { height: 480 color: "#d6d6d6" - property string searchTerms: "" property int inAnimDur: 250 property int counter: 0 property alias isLoading: tweetsModel.isLoading @@ -85,13 +85,15 @@ Rectangle { onTriggered: { main.counter--; var id = tweetsModel.model.get(idx[main.counter]).id - mainListView.add( { "statusText": tweetsModel.model.get(main.counter).content, - "name": tweetsModel.model.get(main.counter).name, - "userImage": tweetsModel.model.get(main.counter).image, - "source": tweetsModel.model.get(main.counter).source, - "id": id, - "uri": tweetsModel.model.get(main.counter).uri, - "published": tweetsModel.model.get(main.counter).published } ); + var item = tweetsModel.model.get(main.counter) + mainListView.add( { "statusText": Helper.insertLinks(item.text, item.entities), + "twitterName": item.user.screen_name, + "name" : item.user.name, + "userImage": item.user.profile_image_url, + "source": item.source, + "id": id, + "uri": Helper.insertLinks(item.user.url, item.user.entities), + "published": item.created_at } ); ids.push(id) } } @@ -107,7 +109,7 @@ Rectangle { PropertyAction { property: "appear"; value: 250 } } - onDragEnded: if (header.refresh) { tweetsModel.model.reload() } + onDragEnded: if (header.refresh) { tweetsModel.reload() } ListHeader { id: header -- 2.7.4