// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.io.File;
import twitter4j.DirectMessage;
import twitter4j.IDs;
import twitter4j.Query;
import twitter4j.Status;
import twitter4j.StatusUpdate;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.User;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.annotations.UsesActivities;
import com.google.appinventor.components.annotations.androidmanifest.ActivityElement;
import com.google.appinventor.components.annotations.androidmanifest.IntentFilterElement;
import com.google.appinventor.components.annotations.androidmanifest.ActionElement;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.AsynchUtil;
import com.google.appinventor.components.runtime.util.ErrorMessages;
/**
* Component for accessing Twitter.
*
* @author sharon@google.com (Sharon Perl) - added OAuth support
* @author ajcolter@gmail.com (Aubrey Colter) - added the twitter4j 2.2.6 jars
* @author josmasflores@gmail.com (Jose Dominguez) - added the twitter4j 3.0.3 jars and fixed auth bug 2413
* @author edwinhzhang@gmail.com (Edwin Zhang) - added twitter4j-media-support-3.03 jar, status + image upload
*/
@DesignerComponent(version = YaVersion.TWITTER_COMPONENT_VERSION, description = "A non-visible component that enables communication "
+ "with <a href=\"http://www.twitter.com\" target=\"_blank\">Twitter</a>. "
+ "Once a user has logged into their Twitter account (and the authorization has been confirmed successful by the "
+ "<code>IsAuthorized</code> event), many more operations are available:<ul>"
+ "<li> Searching Twitter for tweets or labels (<code>SearchTwitter</code>)</li>\n"
+ "<li> Sending a Tweet (<code>Tweet</code>)"
+ " </li>\n"
+ "<li> Sending a Tweet with an Image (<code>TweetWithImage</code>)"
+ " </li>\n"
+ "<li> Directing a message to a specific user "
+ " (<code>DirectMessage</code>)</li>\n "
+ "<li> Receiving the most recent messages directed to the logged-in user "
+ " (<code>RequestDirectMessages</code>)</li>\n "
+ "<li> Following a specific user (<code>Follow</code>)</li>\n"
+ "<li> Ceasing to follow a specific user (<code>StopFollowing</code>)</li>\n"
+ "<li> Getting a list of users following the logged-in user "
+ " (<code>RequestFollowers</code>)</li>\n "
+ "<li> Getting the most recent messages of users followed by the "
+ " logged-in user (<code>RequestFriendTimeline</code>)</li>\n "
+ "<li> Getting the most recent mentions of the logged-in user "
+ " (<code>RequestMentions</code>)</li></ul></p>\n "
+ "<p>You must obtain a Consumer Key and Consumer Secret for Twitter authorization "
+ " specific to your app from http://twitter.com/oauth_clients/new",
category = ComponentCategory.SOCIAL, nonVisible = true, iconName = "images/twitter.png")
@SimpleObject
@UsesPermissions(permissionNames = "android.permission.INTERNET")
@UsesLibraries(libraries = "twitter4j.jar," + "twitter4jmedia.jar")
@UsesActivities(activities = {
@ActivityElement(name = "com.google.appinventor.components.runtime.WebViewActivity",
configChanges = "orientation|keyboardHidden",
screenOrientation = "behind",
intentFilters = {
@IntentFilterElement(actionElements = {
@ActionElement(name = "android.intent.action.MAIN")
})
})
})
public final class Twitter extends AndroidNonvisibleComponent implements
ActivityResultListener, Component {
private static final String ACCESS_TOKEN_TAG = "TwitterOauthAccessToken";
private static final String ACCESS_SECRET_TAG = "TwitterOauthAccessSecret";
private static final String MAX_CHARACTERS = "160";
private static final String URL_HOST = "twitter";
private static final String CALLBACK_URL = Form.APPINVENTOR_URL_SCHEME
+ "://" + URL_HOST;
private static final String WEBVIEW_ACTIVITY_CLASS = WebViewActivity.class
.getName();
// the following fields should only be accessed from the UI thread
private String consumerKey = "";
private String consumerSecret = "";
private String TwitPic_API_Key = "";
private final List<String> mentions;
private final List<String> followers;
private final List<List<String>> timeline;
private final List<String> directMessages;
private final List<String> searchResults;
// the following final fields are not synchronized -- twitter4j is thread
// safe as of 2.2.6
private twitter4j.Twitter twitter;
private RequestToken requestToken;
private AccessToken accessToken;
private String userName = "";
private final SharedPreferences sharedPreferences;
private final int requestCode;
private final ComponentContainer container;
private final Handler handler;
// TODO(sharon): twitter4j apparently has an asynchronous interface
// (AsynchTwitter).
// We should consider whether it has any advantages over AsynchUtil.
/**
* The maximum number of mentions returned by the following methods:
*
* <table>
* <tr>
* <td>component</td>
* <td>twitter4j library</td>
* <td>twitter API</td>
* </tr>
* <tr>
* <td>RequestMentions</td>
* <td>getMentions</td>
* <td>statuses/mentions</td>
* </tr>
* <tr>
* <td>RequestDirectMessages</td>
* <td>getDirectMessages</td>
* <td>direct_messages</td>
* </tr>
* </table>
*/
private static final String MAX_MENTIONS_RETURNED = "20";
public Twitter(ComponentContainer container) {
super(container.$form());
this.container = container;
handler = new Handler();
mentions = new ArrayList<String>();
followers = new ArrayList<String>();
timeline = new ArrayList<List<String>>();
directMessages = new ArrayList<String>();
searchResults = new ArrayList<String>();
sharedPreferences = container.$context().getSharedPreferences("Twitter",
Context.MODE_PRIVATE);
accessToken = retrieveAccessToken();
requestCode = form.registerForActivityResult(this);
}
/**
* Logs in to Twitter with a username and password.
*/
// @Deprecated
// [lyn, 2015/12/30] Removed @Deprecated annotation for this method, which was deprecated in AI1
// by setting userVisible = false. The @Deprecated annotation should only be used for
// events/methods/properties deprecated in AI2. The problem with using it for methods deprecated
// in AI1 is that the names of such methods no longer exist in OdeMessages.java, but the
// AI2 bad blocks mechanism (which uses the @Deprecated annotation) requires the method names
// to exist and be translatable so that they can appear in a block marked bad.
@SimpleFunction(userVisible = false, description = "Twitter's API no longer supports login via username and "
+ "password. Use the Authorize call instead.")
public void Login(String username, String password) {
form.dispatchErrorOccurredEvent(this, "Login",
ErrorMessages.ERROR_TWITTER_UNSUPPORTED_LOGIN_FUNCTION);
}
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "The user name of the authorized user. Empty if "
+ "there is no authorized user.")
public String Username() {
return userName;
}
/**
* ConsumerKey property getter method.
*/
@SimpleProperty(category = PropertyCategory.BEHAVIOR)
public String ConsumerKey() {
return consumerKey;
}
/**
* ConsumerKey property setter method: sets the consumer key to be used when
* authorizing with Twitter via OAuth.
*
* @param consumerKey
* the key for use in Twitter OAuth
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "The the consumer key to be used when authorizing with Twitter via OAuth.")
public void ConsumerKey(String consumerKey) {
this.consumerKey = consumerKey;
}
/**
* ConsumerSecret property getter method.
*/
@SimpleProperty(category = PropertyCategory.BEHAVIOR)
public String ConsumerSecret() {
return consumerSecret;
}
/**
* ConsumerSecret property setter method: sets the consumer secret to be used
* when authorizing with Twitter via OAuth.
*
* @param consumerSecret
* the secret for use in Twitter OAuth
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
@SimpleProperty(description="The consumer secret to be used when authorizing with Twitter via OAuth")
public void ConsumerSecret(String consumerSecret) {
this.consumerSecret = consumerSecret;
}
/**
* TwitPicAPIkey property getter method.
*/
@Deprecated
@SimpleProperty( // [lyn 2015/12/30] removed userVisible = false, which is superseded by @Deprecated
category = PropertyCategory.BEHAVIOR)
public String TwitPic_API_Key() {
return TwitPic_API_Key;
}
/**
* TwitPicAPIkey property setter method: sets the TwitPicAPIkey to be used
* for image uploading with twitter.
*
* @param TwitPic_API_Key
* the API Key for image uploading, given by TwitPic
*/
@Deprecated
// Hide the deprecated property from the Designer
//@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
@SimpleProperty( // [lyn 2015/12/30] removed userVisible = false, which is superseded by @Deprecated
category = PropertyCategory.BEHAVIOR,
description="The API Key for image uploading, provided by TwitPic.")
public void TwitPic_API_Key(String TwitPic_API_Key) {
this.TwitPic_API_Key = TwitPic_API_Key;
}
/**
* Indicates when the login has been successful.
*/
@SimpleEvent(description = "This event is raised after the program calls "
+ "<code>Authorize</code> if the authorization was successful. "
+ "It is also called after a call to <code>CheckAuthorized</code> "
+ "if we already have a valid access token. "
+ "After this event has been raised, any other method for this "
+ "component can be called.")
public void IsAuthorized() {
EventDispatcher.dispatchEvent(this, "IsAuthorized");
}
/**
* Authenticate to Twitter using OAuth
*/
@SimpleFunction(description = "Redirects user to login to Twitter via the Web browser using "
+ "the OAuth protocol if we don't already have authorization.")
public void Authorize() {
if (consumerKey.length() == 0 || consumerSecret.length() == 0) {
form.dispatchErrorOccurredEvent(this, "Authorize",
ErrorMessages.ERROR_TWITTER_BLANK_CONSUMER_KEY_OR_SECRET);
return;
}
if (twitter == null) {
twitter = new TwitterFactory().getInstance();
}
final String myConsumerKey = consumerKey;
final String myConsumerSecret = consumerSecret;
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
if (checkAccessToken(myConsumerKey, myConsumerSecret)) {
handler.post(new Runnable() {
@Override
public void run() {
IsAuthorized();
}
});
return;
}
try {
// potentially time-consuming calls
RequestToken newRequestToken;
twitter.setOAuthConsumer(myConsumerKey, myConsumerSecret);
newRequestToken = twitter.getOAuthRequestToken(CALLBACK_URL);
String authURL = newRequestToken.getAuthorizationURL();
requestToken = newRequestToken; // request token will be
// needed to get access token
Intent browserIntent = new Intent(Intent.ACTION_MAIN, Uri
.parse(authURL));
browserIntent.setClassName(container.$context(),
WEBVIEW_ACTIVITY_CLASS);
container.$context().startActivityForResult(browserIntent,
requestCode);
} catch (TwitterException e) {
Log.i("Twitter", "Got exception: " + e.getMessage());
e.printStackTrace();
form.dispatchErrorOccurredEvent(Twitter.this, "Authorize",
ErrorMessages.ERROR_TWITTER_EXCEPTION, e.getMessage());
DeAuthorize(); // clean up
} catch (IllegalStateException ise){ //This should never happen cause it should return
// at the if (checkAccessToken...). We mark as an error but let continue
Log.e("Twitter", "OAuthConsumer was already set: launch IsAuthorized()");
handler.post(new Runnable() {
@Override
public void run() {
IsAuthorized();
}
});
}
}
});
}
/**
* Check whether we already have a valid Twitter access token
*/
@SimpleFunction(description = "Checks whether we already have access, and if so, causes "
+ "IsAuthorized event handler to be called.")
public void CheckAuthorized() {
final String myConsumerKey = consumerKey;
final String myConsumerSecret = consumerSecret;
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
if (checkAccessToken(myConsumerKey, myConsumerSecret)) {
handler.post(new Runnable() {
@Override
public void run() {
IsAuthorized();
}
});
}
}
});
}
/*
* Get result from starting WebView activity to authorize access
*/
@Override
public void resultReturned(int requestCode, int resultCode, Intent data) {
Log.i("Twitter", "Got result " + resultCode);
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
Log.i("Twitter", "Intent URI: " + uri.toString());
final String oauthVerifier = uri.getQueryParameter("oauth_verifier");
if (twitter == null) {
Log.e("Twitter", "twitter field is unexpectedly null");
form.dispatchErrorOccurredEvent(this, "Authorize",
ErrorMessages.ERROR_TWITTER_UNABLE_TO_GET_ACCESS_TOKEN,
"internal error: can't access Twitter library");
new RuntimeException().printStackTrace();
}
if (requestToken != null && oauthVerifier != null
&& oauthVerifier.length() != 0) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
try {
AccessToken resultAccessToken;
resultAccessToken = twitter.getOAuthAccessToken(requestToken,
oauthVerifier);
accessToken = resultAccessToken;
userName = accessToken.getScreenName();
saveAccessToken(resultAccessToken);
handler.post(new Runnable() {
@Override
public void run() {
IsAuthorized();
}
});
} catch (TwitterException e) {
Log.e("Twitter", "Got exception: " + e.getMessage());
e.printStackTrace();
form.dispatchErrorOccurredEvent(Twitter.this, "Authorize",
ErrorMessages.ERROR_TWITTER_UNABLE_TO_GET_ACCESS_TOKEN,
e.getMessage());
deAuthorize(); // clean up
}
}
});
} else {
form.dispatchErrorOccurredEvent(this, "Authorize",
ErrorMessages.ERROR_TWITTER_AUTHORIZATION_FAILED);
deAuthorize(); // clean up
}
} else {
Log.e("Twitter", "uri returned from WebView activity was unexpectedly null");
deAuthorize(); // clean up so we can call Authorize again
}
} else {
Log.e("Twitter", "intent returned from WebView activity was unexpectedly null");
deAuthorize(); // clean up so we can call Authorize again
}
}
private void saveAccessToken(AccessToken accessToken) {
final SharedPreferences.Editor sharedPrefsEditor = sharedPreferences.edit();
if (accessToken == null) {
sharedPrefsEditor.remove(ACCESS_TOKEN_TAG);
sharedPrefsEditor.remove(ACCESS_SECRET_TAG);
} else {
sharedPrefsEditor.putString(ACCESS_TOKEN_TAG, accessToken.getToken());
sharedPrefsEditor.putString(ACCESS_SECRET_TAG,
accessToken.getTokenSecret());
}
sharedPrefsEditor.commit();
}
private AccessToken retrieveAccessToken() {
String token = sharedPreferences.getString(ACCESS_TOKEN_TAG, "");
String secret = sharedPreferences.getString(ACCESS_SECRET_TAG, "");
if (token.length() == 0 || secret.length() == 0) {
return null;
}
return new AccessToken(token, secret);
}
/**
* Remove authentication for this app instance
*/
@SimpleFunction(description = "Removes Twitter authorization from this running app instance")
public void DeAuthorize() {
deAuthorize();
}
private void deAuthorize() {
final twitter4j.Twitter oldTwitter;
requestToken = null;
accessToken = null;
userName = "";
oldTwitter = twitter;
twitter = null; // setting twitter to null gives us a quick check
// that we don't have an authorized version around.
saveAccessToken(accessToken);
// clear the access token from the old twitter instance, just in case
// someone stashed it away.
if (oldTwitter != null) {
oldTwitter.setOAuthAccessToken(null);
}
}
/**
* Sends a Tweet of the currently logged in user.
*/
@SimpleFunction(description = "This sends a tweet as the logged-in user with the "
+ "specified Text, which will be trimmed if it exceeds "
+ MAX_CHARACTERS
+ " characters. "
+ "<p><u>Requirements</u>: This should only be called after the "
+ "<code>IsAuthorized</code> event has been raised, indicating that the "
+ "user has successfully logged in to Twitter.</p>")
public void Tweet(final String status) {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "Tweet",
ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED, "Need to login?");
return;
}
// TODO(sharon): note that if the user calls DeAuthorize immediately
// after
// Tweet it is possible that the DeAuthorize call can slip in
// and invalidate the authorization credentials for myTwitter, causing
// the call below to fail. If we want to prevent this we could consider
// using an ExecutorService object to serialize calls to Twitter.
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
try {
twitter.updateStatus(status);
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "Tweet",
ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED, e.getMessage());
}
}
});
}
/**
* Tweet with Image, Uploaded to Twitter
*/
@SimpleFunction(description = "This sends a tweet as the logged-in user with the "
+ "specified Text and a path to the image to be uploaded, which will be trimmed if it "
+ "exceeds " + MAX_CHARACTERS + " characters. "
+ "If an image is not found or invalid, only the text will be tweeted."
+ "<p><u>Requirements</u>: This should only be called after the "
+ "<code>IsAuthorized</code> event has been raised, indicating that the "
+ "user has successfully logged in to Twitter.</p>" )
public void TweetWithImage(final String status, final String imagePath) {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "TweetWithImage",
ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED, "Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
try {
String cleanImagePath = imagePath;
// Clean up the file path if necessary
if (cleanImagePath.startsWith("file://")) {
cleanImagePath = imagePath.replace("file://", "");
}
File imageFilePath = new File(cleanImagePath);
if (imageFilePath.exists()) {
StatusUpdate theTweet = new StatusUpdate(status);
theTweet.setMedia(imageFilePath);
twitter.updateStatus(theTweet);
}
else {
form.dispatchErrorOccurredEvent(Twitter.this, "TweetWithImage",
ErrorMessages.ERROR_TWITTER_INVALID_IMAGE_PATH);
}
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "TweetWithImage",
ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED, e.getMessage());
}
}
});
}
/**
* Gets the most recent messages where your username is mentioned.
*/
@SimpleFunction(description = "Requests the " + MAX_MENTIONS_RETURNED
+ " most "
+ "recent mentions of the logged-in user. When the mentions have been "
+ "retrieved, the system will raise the <code>MentionsReceived</code> "
+ "event and set the <code>Mentions</code> property to the list of "
+ "mentions."
+ "<p><u>Requirements</u>: This should only be called after the "
+ "<code>IsAuthorized</code> event has been raised, indicating that the "
+ "user has successfully logged in to Twitter.</p>")
public void RequestMentions() {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "RequestMentions",
ErrorMessages.ERROR_TWITTER_REQUEST_MENTIONS_FAILED, "Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
List<Status> replies = Collections.emptyList();
public void run() {
try {
replies = twitter.getMentionsTimeline();
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "RequestMentions",
ErrorMessages.ERROR_TWITTER_REQUEST_MENTIONS_FAILED,
e.getMessage());
} finally {
handler.post(new Runnable() {
public void run() {
mentions.clear();
for (Status status : replies) {
mentions.add(status.getUser().getScreenName() + " "
+ status.getText());
}
MentionsReceived(mentions);
}
});
}
}
});
}
/**
* Indicates when all the mentions requested through
* {@link #RequestMentions()} have been received.
*/
@SimpleEvent(description = "This event is raised when the mentions of the logged-in user "
+ "requested through <code>RequestMentions</code> have been retrieved. "
+ "A list of the mentions can then be found in the <code>mentions</code> "
+ "parameter or the <code>Mentions</code> property.")
public void MentionsReceived(final List<String> mentions) {
EventDispatcher.dispatchEvent(this, "MentionsReceived", mentions);
}
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains a list of mentions of the "
+ "logged-in user. Initially, the list is empty. To set it, the "
+ "program must: <ol> "
+ "<li> Call the <code>Authorize</code> method.</li> "
+ "<li> Wait for the <code>IsAuthorized</code> event.</li> "
+ "<li> Call the <code>RequestMentions</code> method.</li> "
+ "<li> Wait for the <code>MentionsReceived</code> event.</li></ol>\n"
+ "The value of this property will then be set to the list of mentions "
+ "(and will maintain its value until any subsequent calls to "
+ "<code>RequestMentions</code>).")
public List<String> Mentions() {
return mentions;
}
/**
* Gets who is following you.
*/
@SimpleFunction
public void RequestFollowers() {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "RequestFollowers",
ErrorMessages.ERROR_TWITTER_REQUEST_FOLLOWERS_FAILED,
"Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
List<User> friends = new ArrayList<User>();
public void run() {
try {
IDs followerIDs = twitter.getFollowersIDs(-1);
for (long id : followerIDs.getIDs()) {
// convert from the IDs returned to the User
friends.add(twitter.showUser(id));
}
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "RequestFollowers",
ErrorMessages.ERROR_TWITTER_REQUEST_FOLLOWERS_FAILED,
e.getMessage());
} finally {
handler.post(new Runnable() {
public void run() {
followers.clear();
for (User user : friends) {
followers.add(user.getName());
}
FollowersReceived(followers);
}
});
}
}
});
}
/**
* Indicates when all of your followers requested through
* {@link #RequestFollowers()} have been received.
*/
@SimpleEvent(description = "This event is raised when all of the followers of the "
+ "logged-in user requested through <code>RequestFollowers</code> have "
+ "been retrieved. A list of the followers can then be found in the "
+ "<code>followers</code> parameter or the <code>Followers</code> "
+ "property.")
public void FollowersReceived(final List<String> followers2) {
EventDispatcher.dispatchEvent(this, "FollowersReceived", followers2);
}
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains a list of the followers of the "
+ "logged-in user. Initially, the list is empty. To set it, the "
+ "program must: <ol> "
+ "<li> Call the <code>Authorize</code> method.</li> "
+ "<li> Wait for the <code>IsAuthorized</code> event.</li> "
+ "<li> Call the <code>RequestFollowers</code> method.</li> "
+ "<li> Wait for the <code>FollowersReceived</code> event.</li></ol>\n"
+ "The value of this property will then be set to the list of "
+ "followers (and maintain its value until any subsequent call to "
+ "<code>RequestFollowers</code>).")
public List<String> Followers() {
return followers;
}
/**
* Gets the most recent messages sent directly to you.
*/
@SimpleFunction(description = "Requests the " + MAX_MENTIONS_RETURNED
+ " most "
+ "recent direct messages sent to the logged-in user. When the "
+ "messages have been retrieved, the system will raise the "
+ "<code>DirectMessagesReceived</code> event and set the "
+ "<code>DirectMessages</code> property to the list of messages."
+ "<p><u>Requirements</u>: This should only be called after the "
+ "<code>IsAuthorized</code> event has been raised, indicating that the "
+ "user has successfully logged in to Twitter.</p>")
public void RequestDirectMessages() {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "RequestDirectMessages",
ErrorMessages.ERROR_TWITTER_REQUEST_DIRECT_MESSAGES_FAILED,
"Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
List<DirectMessage> messages = Collections.emptyList();
@Override
public void run() {
try {
messages = twitter.getDirectMessages();
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this,
"RequestDirectMessages",
ErrorMessages.ERROR_TWITTER_REQUEST_DIRECT_MESSAGES_FAILED,
e.getMessage());
} finally {
handler.post(new Runnable() {
@Override
public void run() {
directMessages.clear();
for (DirectMessage message : messages) {
directMessages.add(message.getSenderScreenName() + " "
+ message.getText());
}
DirectMessagesReceived(directMessages);
}
});
}
}
});
}
/**
* Indicates when all the direct messages requested through
* {@link #RequestDirectMessages()} have been received.
*/
@SimpleEvent(description = "This event is raised when the recent messages "
+ "requested through <code>RequestDirectMessages</code> have "
+ "been retrieved. A list of the messages can then be found in the "
+ "<code>messages</code> parameter or the <code>Messages</code> "
+ "property.")
public void DirectMessagesReceived(final List<String> messages) {
EventDispatcher.dispatchEvent(this, "DirectMessagesReceived", messages);
}
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains a list of the most recent "
+ "messages mentioning the logged-in user. Initially, the list is "
+ "empty. To set it, the program must: <ol> "
+ "<li> Call the <code>Authorize</code> method.</li> "
+ "<li> Wait for the <code>Authorized</code> event.</li> "
+ "<li> Call the <code>RequestDirectMessages</code> method.</li> "
+ "<li> Wait for the <code>DirectMessagesReceived</code> event.</li>"
+ "</ol>\n"
+ "The value of this property will then be set to the list of direct "
+ "messages retrieved (and maintain that value until any subsequent "
+ "call to <code>RequestDirectMessages</code>).")
public List<String> DirectMessages() {
return directMessages;
}
/**
* Sends a direct message to a specified username.
*/
@SimpleFunction(description = "This sends a direct (private) message to the specified "
+ "user. The message will be trimmed if it exceeds "
+ MAX_CHARACTERS
+ "characters. "
+ "<p><u>Requirements</u>: This should only be called after the "
+ "<code>IsAuthorized</code> event has been raised, indicating that the "
+ "user has successfully logged in to Twitter.</p>")
public void DirectMessage(final String user, final String message) {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "DirectMessage",
ErrorMessages.ERROR_TWITTER_DIRECT_MESSAGE_FAILED, "Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
try {
twitter.sendDirectMessage(user, message);
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "DirectMessage",
ErrorMessages.ERROR_TWITTER_DIRECT_MESSAGE_FAILED, e.getMessage());
}
}
});
}
/**
* Starts following a user.
*/
@SimpleFunction
public void Follow(final String user) {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "Follow",
ErrorMessages.ERROR_TWITTER_FOLLOW_FAILED, "Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
try {
twitter.createFriendship(user);
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "Follow",
ErrorMessages.ERROR_TWITTER_FOLLOW_FAILED, e.getMessage());
}
}
});
}
/**
* Stops following a user.
*/
@SimpleFunction
public void StopFollowing(final String user) {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "StopFollowing",
ErrorMessages.ERROR_TWITTER_STOP_FOLLOWING_FAILED, "Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
try {
twitter.destroyFriendship(user);
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "StopFollowing",
ErrorMessages.ERROR_TWITTER_STOP_FOLLOWING_FAILED, e.getMessage());
}
}
});
}
/**
* Gets the most recent 20 messages in the user's timeline.
*/
@SimpleFunction
public void RequestFriendTimeline() {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "RequestFriendTimeline",
ErrorMessages.ERROR_TWITTER_REQUEST_FRIEND_TIMELINE_FAILED,
"Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
List<Status> messages = Collections.emptyList();
public void run() {
try {
messages = twitter.getHomeTimeline();
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this,
"RequestFriendTimeline",
ErrorMessages.ERROR_TWITTER_REQUEST_FRIEND_TIMELINE_FAILED,
e.getMessage());
} finally {
handler.post(new Runnable() {
public void run() {
timeline.clear();
for (Status message : messages) {
List<String> status = new ArrayList<String>();
status.add(message.getUser().getScreenName());
status.add(message.getText());
timeline.add(status);
}
FriendTimelineReceived(timeline);
}
});
}
}
});
}
/**
* Indicates when the friend timeline requested through
* {@link #RequestFriendTimeline()} has been received.
*/
@SimpleEvent(description = "This event is raised when the messages "
+ "requested through <code>RequestFriendTimeline</code> have "
+ "been retrieved. The <code>timeline</code> parameter and the "
+ "<code>Timeline</code> property will contain a list of lists, where "
+ "each sub-list contains a status update of the form (username message)")
public void FriendTimelineReceived(final List<List<String>> timeline) {
EventDispatcher.dispatchEvent(this, "FriendTimelineReceived", timeline);
}
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains the 20 most recent messages of "
+ "users being followed. Initially, the list is empty. To set it, "
+ "the program must: <ol> "
+ "<li> Call the <code>Authorize</code> method.</li> "
+ "<li> Wait for the <code>IsAuthorized</code> event.</li> "
+ "<li> Specify users to follow with one or more calls to the "
+ "<code>Follow</code> method.</li> "
+ "<li> Call the <code>RequestFriendTimeline</code> method.</li> "
+ "<li> Wait for the <code>FriendTimelineReceived</code> event.</li> "
+ "</ol>\n"
+ "The value of this property will then be set to the list of messages "
+ "(and maintain its value until any subsequent call to "
+ "<code>RequestFriendTimeline</code>.")
public List<List<String>> FriendTimeline() {
return timeline;
}
/**
* Search for tweets or labels
*/
@SimpleFunction(description = "This searches Twitter for the given String query."
+ "<p><u>Requirements</u>: This should only be called after the "
+ "<code>IsAuthorized</code> event has been raised, indicating that the "
+ "user has successfully logged in to Twitter.</p>")
public void SearchTwitter(final String query) {
if (twitter == null || userName.length() == 0) {
form.dispatchErrorOccurredEvent(this, "SearchTwitter",
ErrorMessages.ERROR_TWITTER_SEARCH_FAILED, "Need to login?");
return;
}
AsynchUtil.runAsynchronously(new Runnable() {
List<Status> tweets = Collections.emptyList();
public void run() {
try {
tweets = twitter.search(new Query(query)).getTweets();
} catch (TwitterException e) {
form.dispatchErrorOccurredEvent(Twitter.this, "SearchTwitter",
ErrorMessages.ERROR_TWITTER_SEARCH_FAILED, e.getMessage());
} finally {
handler.post(new Runnable() {
public void run() {
searchResults.clear();
for (Status tweet : tweets) {
searchResults.add(tweet.getUser().getName() + " " + tweet.getText());
}
SearchSuccessful(searchResults);
}
});
}
}
});
}
/**
* Indicates when the search requested through {@link #SearchTwitter(String)}
* has completed.
*/
@SimpleEvent(description = "This event is raised when the results of the search "
+ "requested through <code>SearchSuccessful</code> have "
+ "been retrieved. A list of the results can then be found in the "
+ "<code>results</code> parameter or the <code>Results</code> "
+ "property.")
public void SearchSuccessful(final List<String> searchResults) {
EventDispatcher.dispatchEvent(this, "SearchSuccessful", searchResults);
}
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property, which is initially empty, is set to a "
+ "list of search results after the program: <ol>"
+ "<li>Calls the <code>SearchTwitter</code> method.</li> "
+ "<li>Waits for the <code>SearchSuccessful</code> event.</li></ol>\n"
+ "The value of the property will then be the same as the parameter to "
+ "<code>SearchSuccessful</code>. Note that it is not necessary to "
+ "call the <code>Authorize</code> method before calling "
+ "<code>SearchTwitter</code>.")
public List<String> SearchResults() {
return searchResults;
}
/**
* Check whether accessToken is stored in preferences. If there is one, set it.
* If it was already set (for instance calling Authorize twice in a row),
* it will throw an IllegalStateException that, in this case, can be ignored.
* @return true if accessToken is valid and set (user authorized), false otherwise.
*/
private boolean checkAccessToken(String myConsumerKey, String myConsumerSecret) {
accessToken = retrieveAccessToken();
if (accessToken == null) {
return false;
}
else {
if (twitter == null) {
twitter = new TwitterFactory().getInstance();
}
try {
twitter.setOAuthConsumer(consumerKey, consumerSecret);
twitter.setOAuthAccessToken(accessToken);
}
catch (IllegalStateException ies) {
//ignore: it means that the consumer data was already set
}
if (userName.trim().length() == 0) {
User user;
try {
user = twitter.verifyCredentials();
userName = user.getScreenName();
} catch (TwitterException e) {// something went wrong (networks or bad credentials <-- DeAuthorize
deAuthorize();
return false;
}
}
return true;
}
}
}