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 <aalpert@blackberry.com>
This commit is contained in:
Topi Reinio 2013-06-21 10:50:48 +02:00 committed by The Qt Project
parent 1099b26535
commit c9ec0c2065
6 changed files with 187 additions and 60 deletions

View File

@ -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

View File

@ -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 reload() {
tweets.clear()
if (from == "" && phrase == "")
return;
//! [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]
}
onPhraseChanged: reload();
onFromChanged: reload();
Component.onCompleted: {
if (consumerKey === "" || consumerSecret == "") {
bearerToken = encodeURIComponent(Helper.demoToken())
return;
}
function encodePhrase(x) { return encodeURIComponent(x); }
source: (from == "" && phrase == "") ? "" :
'http://search.twitter.com/search.atom?from='+from+"&rpp=10&phrase="+encodePhrase(phrase)
namespaceDeclarations: "declare default element namespace 'http://www.w3.org/2005/Atom'; " +
"declare namespace twitter=\"http://api.twitter.com/\";";
query: "/feed/entry"
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()" }
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");
}
}

View File

@ -1,19 +1,62 @@
.pragma library
function twitterName(str)
{
var s = str.split("(")
return s[0]
}
function realName(str)
{
var s = str.split("(")
return s[1].substring(0, s[1].length-1)
}
function formatDate(date)
{
var da = new Date(date)
return da.toDateString()
}
function demoToken()
{
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 linkForEntity(entity)
{
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) +
'<a href=\"' + linkForEntity(links[i]) + '\">' +
textForEntity(links[i]) + '</a>' +
text.substring(links[i].indices[1])
}
return text.replace(/\n/g, '<br>');
}

View File

@ -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
*/

View File

@ -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

View File

@ -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