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:
parent
1099b26535
commit
c9ec0c2065
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>');
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue