Update tweeting api in study plugins

* Create separate extension point to configure twitter
* Show tweet panel before asking to authorize
* Add input verifier to twitter panel
This commit is contained in:
Valentina Kiryushkina
2016-03-22 17:12:43 +03:00
parent b08105757f
commit f7f46ebdf0
7 changed files with 268 additions and 213 deletions
@@ -36,6 +36,7 @@
<with attribute="implementationClass" implements="com.jetbrains.edu.learning.StudyLanguageManager"/>
</extensionPoint>
<extensionPoint qualifiedName="Edu.studyPluginConfigurator" interface="com.jetbrains.edu.learning.StudyPluginConfigurator"/>
<extensionPoint qualifiedName="Edu.studyTwitterPluginConfigurator" interface="com.jetbrains.edu.learning.StudyTwitterPluginConfigurator"/>
</extensionPoints>
<actions>
@@ -7,10 +7,8 @@ import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.jetbrains.edu.learning.actions.*;
import com.jetbrains.edu.learning.courseFormat.StudyStatus;
import com.jetbrains.edu.learning.courseFormat.Task;
import com.jetbrains.edu.learning.courseFormat.TaskFile;
import com.jetbrains.edu.learning.twitter.StudyTwitterUtils;
import com.jetbrains.edu.learning.ui.StudyToolWindow;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -100,44 +98,4 @@ public abstract class StudyBasePluginConfigurator implements StudyPluginConfigur
public StudyAfterCheckAction[] getAfterCheckActions() {
return null;
}
@NotNull
@Override
public String getConsumerKey(@NotNull Project project) {
return "";
}
@NotNull
@Override
public String getConsumerSecret(@NotNull Project project) {
return "";
}
@Override
public void storeTwitterTokens(@NotNull Project project, @NotNull String accessToken, @NotNull String tokenSecret) {
// do nothing
}
@NotNull
@Override
public String getTwitterTokenSecret(@NotNull Project project) {
return "";
}
@NotNull
@Override
public String getTwitterAccessToken(@NotNull Project project) {
return "";
}
@Override
public boolean askToTweet(@NotNull Project project, Task solvedTask, StudyStatus statusBeforeCheck) {
return false;
}
@Nullable
@Override
public StudyTwitterUtils.TwitterDialogPanel getTweetDialogPanel(@NotNull Task solvedTask) {
return null;
}
}
@@ -5,10 +5,7 @@ import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.project.Project;
import com.jetbrains.edu.learning.actions.StudyAfterCheckAction;
import com.jetbrains.edu.learning.courseFormat.StudyStatus;
import com.jetbrains.edu.learning.courseFormat.Task;
import com.jetbrains.edu.learning.settings.ModifiableSettingsPanel;
import com.jetbrains.edu.learning.twitter.StudyTwitterUtils;
import com.jetbrains.edu.learning.ui.StudyToolWindow;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -21,7 +18,6 @@ public interface StudyPluginConfigurator {
/**
* Provide action group that should be placed on the tool window toolbar.
* @return
*/
@NotNull
DefaultActionGroup getActionGroup(Project project);
@@ -37,7 +33,6 @@ public interface StudyPluginConfigurator {
FileEditorManagerListener getFileEditorManagerListener(@NotNull final Project project, @NotNull final StudyToolWindow toolWindow);
/**
*
* @return parameter for CodeMirror script. Available languages: @see <@linktourl http://codemirror.net/mode/>
*/
@NotNull String getDefaultHighlightingMode();
@@ -49,47 +44,6 @@ public interface StudyPluginConfigurator {
@Nullable
ModifiableSettingsPanel getSettingsPanel();
/**
* To implement tweeting you should register you app in twitter. For registered application twitter provide
* consumer key and consumer secret, that are used for authorize by OAuth.
* @return consumer key for current educational plugin
*/
@NotNull String getConsumerKey(@NotNull final Project project);
/**
* To implement tweeting you should register you app in twitter. For registered application twitter provide
* consumer key and consumer secret, that are used for authorize by OAuth.
* @return consumer secret for current educational plugin
*/
@NotNull String getConsumerSecret(@NotNull final Project project);
/**
* The plugin implemented tweeting should define policy when user will be asked to tweet.
*@param statusBeforeCheck @return
*/
boolean askToTweet(@NotNull final Project project, Task solvedTask, StudyStatus statusBeforeCheck);
/**
* Stores access token and token secret, obtained by authorizing PyCharm.
*/
void storeTwitterTokens(@NotNull final Project project, @NotNull final String accessToken, @NotNull final String tokenSecret);
/**
* @return stored access token
*/
@NotNull String getTwitterAccessToken(@NotNull Project project);
/**
* @return stored token secret
*/
@NotNull String getTwitterTokenSecret(@NotNull Project project);
/**
* @return panel that will be shown to user in ask to tweet dialog.
*/
@Nullable
StudyTwitterUtils.TwitterDialogPanel getTweetDialogPanel(@NotNull Task solvedTask);
boolean accept(@NotNull final Project project);
}
@@ -0,0 +1,59 @@
package com.jetbrains.edu.learning;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.jetbrains.edu.learning.courseFormat.StudyStatus;
import com.jetbrains.edu.learning.courseFormat.Task;
import com.jetbrains.edu.learning.twitter.StudyTwitterUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface StudyTwitterPluginConfigurator {
ExtensionPointName<StudyTwitterPluginConfigurator> EP_NAME = ExtensionPointName.create("Edu.studyTwitterPluginConfigurator");
/**
* To implement tweeting you should register you app in twitter. For registered application twitter provide
* consumer key and consumer secret, that are used for authorize by OAuth.
* @return consumer key for current educational plugin
*/
@NotNull
String getConsumerKey(@NotNull final Project project);
/**
* To implement tweeting you should register you app in twitter. For registered application twitter provide
* consumer key and consumer secret, that are used for authorize by OAuth.
* @return consumer secret for current educational plugin
*/
@NotNull String getConsumerSecret(@NotNull final Project project);
/**
* The plugin implemented tweeting should define policy when user will be asked to tweet.
*@param statusBeforeCheck @return
*/
boolean askToTweet(@NotNull final Project project, Task solvedTask, StudyStatus statusBeforeCheck);
/**
* Stores access token and token secret, obtained by authorizing PyCharm.
*/
void storeTwitterTokens(@NotNull final Project project, @NotNull final String accessToken, @NotNull final String tokenSecret);
/**
* @return stored access token
*/
@NotNull String getTwitterAccessToken(@NotNull Project project);
/**
* @return stored token secret
*/
@NotNull String getTwitterTokenSecret(@NotNull Project project);
/**
* @return panel that will be shown to user in ask to tweet dialog.
*/
@Nullable
StudyTwitterUtils.TwitterDialogPanel getTweetDialogPanel(@NotNull Task solvedTask);
void setAskToTweet(@NotNull Project project, boolean askToTweet);
boolean accept(@NotNull final Project project);
}
@@ -36,12 +36,12 @@ import com.intellij.ui.JBColor;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.content.Content;
import com.intellij.util.ui.UIUtil;
import com.jetbrains.edu.learning.checker.StudyExecutor;
import com.jetbrains.edu.learning.checker.StudyTestRunner;
import com.jetbrains.edu.learning.core.EduAnswerPlaceholderDeleteHandler;
import com.jetbrains.edu.learning.core.EduAnswerPlaceholderPainter;
import com.jetbrains.edu.learning.core.EduNames;
import com.jetbrains.edu.learning.core.EduUtils;
import com.jetbrains.edu.learning.checker.StudyExecutor;
import com.jetbrains.edu.learning.checker.StudyTestRunner;
import com.jetbrains.edu.learning.courseFormat.*;
import com.jetbrains.edu.learning.editor.StudyEditor;
import com.jetbrains.edu.learning.ui.StudyProgressToolWindowFactory;
@@ -460,6 +460,18 @@ public class StudyUtils {
return null;
}
@Nullable
public static StudyTwitterPluginConfigurator getTwitterConfigurator(@NotNull final Project project) {
StudyTwitterPluginConfigurator[] extensions = StudyTwitterPluginConfigurator.EP_NAME.getExtensions();
for (StudyTwitterPluginConfigurator extension: extensions) {
if (extension.accept(project)) {
return extension;
}
}
return null;
}
public static String getTaskText(@NotNull final Project project) {
VirtualFile[] files = FileEditorManager.getInstance(project).getSelectedFiles();
TaskFile taskFile = null;
@@ -2,14 +2,11 @@ package com.jetbrains.edu.learning.twitter;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.jetbrains.edu.learning.StudyPluginConfigurator;
import com.jetbrains.edu.learning.StudyUtils;
import com.jetbrains.edu.learning.StudyTwitterPluginConfigurator;
import com.jetbrains.edu.learning.actions.StudyAfterCheckAction;
import com.jetbrains.edu.learning.courseFormat.StudyStatus;
import com.jetbrains.edu.learning.courseFormat.Task;
import org.jetbrains.annotations.NotNull;
import twitter4j.Twitter;
import twitter4j.TwitterException;
/**
* Action that provide tweeting functionality to plugin.
@@ -20,30 +17,17 @@ import twitter4j.TwitterException;
*/
public class StudyTwitterAction extends StudyAfterCheckAction {
Logger LOG = Logger.getInstance(StudyTwitterAction.class);
private StudyTwitterPluginConfigurator myConfigurator;
public StudyTwitterAction(@NotNull final StudyTwitterPluginConfigurator configurator) {
myConfigurator = configurator;
}
@Override
public void run(@NotNull Project project, @NotNull Task solvedTask, StudyStatus statusBeforeCheck) {
try {
StudyPluginConfigurator configurator = StudyUtils.getConfigurator(project);
if (configurator == null) {
LOG.warn("Plugin configurator not found");
return;
}
if (configurator.askToTweet(project, solvedTask, statusBeforeCheck)) {
boolean isAuthorized = !configurator.getTwitterAccessToken(project).isEmpty();
Twitter twitter = StudyTwitterUtils.getTwitter(configurator.getConsumerKey(project), configurator.getConsumerSecret(project));
StudyTwitterUtils.configureTwitter(twitter, project, isAuthorized);
StudyTwitterUtils.TwitterDialogPanel panel = configurator.getTweetDialogPanel(solvedTask);
if (panel != null) {
StudyTwitterUtils.showPostTweetDialogAndPostTweet(twitter, panel);
}
else {
LOG.warn("Plugin didn't provide twitter panel");
}
}
}
catch (TwitterException e) {
LOG.warn(e.getMessage());
if (myConfigurator.askToTweet(project, solvedTask, statusBeforeCheck)) {
StudyTwitterUtils.createTwitterDialogAndShow(project, myConfigurator, solvedTask);
}
}
}
@@ -4,13 +4,17 @@ import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.InputValidatorEx;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.components.JBScrollPane;
import com.jetbrains.edu.learning.StudyPluginConfigurator;
import com.jetbrains.edu.learning.StudyTwitterPluginConfigurator;
import com.jetbrains.edu.learning.StudyUtils;
import com.jetbrains.edu.learning.courseFormat.Task;
import org.apache.http.HttpStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import twitter4j.StatusUpdate;
@@ -24,6 +28,7 @@ import twitter4j.conf.ConfigurationBuilder;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -32,32 +37,10 @@ import java.io.InputStream;
public class StudyTwitterUtils {
private static final Logger LOG = Logger.getInstance(StudyTwitterUtils.class);
/**
* Configure twitter instance: authorize if needed or set access token and token secret provided by configurator.
* @param twitter
* @param project
* @param isAuthorized
* @throws TwitterException
*/
public static void configureTwitter(@NotNull final Twitter twitter, @NotNull final Project project,
final boolean isAuthorized) throws TwitterException {
if (!isAuthorized) {
authorize(project, twitter);
}
else {
StudyPluginConfigurator configurator = StudyUtils.getConfigurator(project);
if (configurator != null) {
getTwitterForAuthorizedApp(twitter, configurator.getTwitterAccessToken(project), configurator.getTwitterTokenSecret(project));
}
}
}
/**
* Set consumer key and secret.
* @param consumerKey
* @param consumerSecret
* @return
* @return Twitter instance with consumer key and secret set.
*/
@NotNull
public static Twitter getTwitter(@NotNull final String consumerKey, @NotNull final String consumerSecret) {
@@ -69,116 +52,222 @@ public class StudyTwitterUtils {
/**
* Set access token and token secret in Twitter instance
* @param twitter
* @param accessToken
* @param tokenSecret
*/
private static void getTwitterForAuthorizedApp(Twitter twitter, @NotNull String accessToken,
@NotNull String tokenSecret) {
private static void setAuthInfoInTwitter(Twitter twitter, @NotNull String accessToken,
@NotNull String tokenSecret) {
AccessToken token = new AccessToken(accessToken, tokenSecret);
twitter.setOAuthAccessToken(token);
}
/**
* Authorize user and save tokens by StudyPluginConfigurator#storeTwitterTokens
* @param project
* @param twitter
* @throws TwitterException
*/
public static void authorize(@NotNull final Project project, @NotNull final Twitter twitter) throws TwitterException {
RequestToken requestToken = twitter.getOAuthRequestToken();
BrowserUtil.browse(requestToken.getAuthorizationURL());
public static void createTwitterDialogAndShow(@NotNull Project project,
@NotNull final StudyTwitterPluginConfigurator configurator,
@NotNull Task task) {
ApplicationManager.getApplication().invokeLater(() -> {
String pin = Messages.showInputDialog("Twitter PIN:", "Twitter Authorization", null, "", null);
try {
AccessToken token;
if (pin != null && pin.length() > 0) {
token = twitter.getOAuthAccessToken(requestToken, pin);
DialogWrapper.DoNotAskOption doNotAskOption = createDoNotAskOption(project, configurator);
StudyTwitterUtils.TwitterDialogPanel panel = configurator.getTweetDialogPanel(task);
if (panel != null) {
TwitterDialogWrapper wrapper = new TwitterDialogWrapper(project, panel, doNotAskOption);
wrapper.setDoNotAskOption(doNotAskOption);
panel.addTextFieldVerifier(createTextFieldLengthDocumentListener(wrapper, panel));
if (wrapper.showAndGet()) {
try {
boolean isAuthorized = !configurator.getTwitterAccessToken(project).isEmpty();
Twitter twitter = getTwitter(configurator.getConsumerKey(project), configurator.getConsumerSecret(project));
if (!isAuthorized) {
authorizeAndUpdateStatus(project, twitter, panel);
}
else {
setAuthInfoInTwitter(twitter, configurator.getTwitterAccessToken(project), configurator.getTwitterTokenSecret(project));
updateStatus(panel, twitter);
}
}
catch (TwitterException | IOException e) {
LOG.warn(e.getMessage());
Messages.showErrorDialog("Status wasn\'t updated. Please, check internet connection and try again", "Twitter");
}
}
else {
token = twitter.getOAuthAccessToken();
}
StudyPluginConfigurator configurator = StudyUtils.getConfigurator(project);
if (configurator != null) {
configurator.storeTwitterTokens(project, token.getToken(), token.getTokenSecret());
}
else {
LOG.warn("Plugin configurator not found");
}
}
catch (TwitterException e) {
if (401 == e.getStatusCode()) {
LOG.warn("Unable to get the access token.");
}
else {
LOG.warn(e.getMessage());
LOG.warn("Panel is null");
}
}
});
}
private static DialogWrapper.DoNotAskOption createDoNotAskOption(@NotNull final Project project,
@NotNull final StudyTwitterPluginConfigurator configurator) {
return new DialogWrapper.DoNotAskOption() {
@Override
public boolean isToBeShown() {
return true;
}
@Override
public void setToBeShown(boolean toBeShown, int exitCode) {
if (exitCode == DialogWrapper.CANCEL_EXIT_CODE || exitCode == DialogWrapper.OK_EXIT_CODE) {
configurator.setAskToTweet(project, toBeShown);
}
}
@Override
public boolean canBeHidden() {
return true;
}
@Override
public boolean shouldSaveOptionsOnCancel() {
return true;
}
@NotNull
@Override
public String getDoNotShowMessage() {
return "Never ask me to tweet";
}
};
}
/**
* Post on twitter media and text from panel
* @param panel shown to user and used to provide data to post
*/
public static void updateStatus(StudyTwitterUtils.TwitterDialogPanel panel, Twitter twitter) throws IOException, TwitterException {
StatusUpdate update = new StatusUpdate(panel.getMessage());
InputStream e = panel.getMediaSource();
if (e != null) {
File imageFile = FileUtil.createTempFile("twitter_media", panel.getMediaExtension());
FileUtil.copy(e, new FileOutputStream(imageFile));
update.media(imageFile);
}
twitter.updateStatus(update);
BrowserUtil.browse("https://twitter.com/");
}
/**
* Show twitter dialog, asking user to tweet about his achievements. Post tweet with provided by panel
* media and text.
* As a result of succeeded tweet twitter website is opened in default browser.
* @param twitter
* @param twitterDialogPanel
*/
public static void showPostTweetDialogAndPostTweet(@NotNull Twitter twitter, @NotNull final TwitterDialogPanel twitterDialogPanel) {
public static void authorizeAndUpdateStatus(@NotNull final Project project, @NotNull final Twitter twitter,
@NotNull final StudyTwitterUtils.TwitterDialogPanel panel) throws TwitterException {
RequestToken requestToken = twitter.getOAuthRequestToken();
BrowserUtil.browse(requestToken.getAuthorizationURL());
ApplicationManager.getApplication().invokeLater(() -> {
DialogBuilder builder = new DialogBuilder();
twitterDialogPanel.addTextFieldVerifier(createTextFieldLengthDocumentListener(builder, twitterDialogPanel));
builder.title("Twitter");
builder.addOkAction().setText("Tweet");
builder.addCancelAction();
builder.setCenterPanel(new JBScrollPane(twitterDialogPanel));
builder.resizable(true);
if (builder.showAndGet()) {
StatusUpdate update = new StatusUpdate(twitterDialogPanel.getMessage());
String pin = createAndShowPinDialog();
if (pin != null) {
try {
InputStream inputStream = twitterDialogPanel.getMediaSource();
if (inputStream != null) {
File imageFile = FileUtil.createTempFile("twitter_media", "gif");
FileUtil.copy(inputStream, new FileOutputStream(imageFile));
update.media(imageFile);
AccessToken token = twitter.getOAuthAccessToken(requestToken, pin);
StudyTwitterPluginConfigurator configurator = StudyUtils.getTwitterConfigurator(project);
if (configurator != null) {
configurator.storeTwitterTokens(project, token.getToken(), token.getTokenSecret());
updateStatus(panel, twitter);
}
else {
LOG.warn("No twitter configurator is provided for the plugin");
}
twitter.updateStatus(update);
BrowserUtil.browse("https://twitter.com/");
}
catch (IOException | TwitterException e) {
catch (TwitterException e) {
if (e.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
LOG.warn("Unable to get the access token.");
LOG.warn(e.getMessage());
}
}
catch (IOException e) {
LOG.warn(e.getMessage());
Messages.showErrorDialog("Status wasn't updated. Please, check internet connection and try again", "Twitter");
}
}
});
}
public static String createAndShowPinDialog() {
return Messages.showInputDialog("Twitter PIN:", "Twitter Authorization", null, "", new InputValidatorEx() {
@Nullable
@Override
public String getErrorText(String inputString) {
inputString = inputString.trim();
if (inputString.isEmpty()) {
return "PIN shouldn't be empty.";
}
if (!isNumeric(inputString)) {
return "PIN should be numeric.";
}
return null;
}
@Override
public boolean checkInput(String inputString) {
return getErrorText(inputString) == null;
}
@Override
public boolean canClose(String inputString) {
return true;
}
private boolean isNumeric(@NotNull final String string) {
for (char c: string.toCharArray()) {
if (!StringUtil.isDecimalDigit(c)) {
return false;
}
}
return true;
}
});
}
/**
* Listener updates label indicating remaining symbols number like in twitter.
* @param builder
* @param panel
* @return
*/
public static DocumentListener createTextFieldLengthDocumentListener(@NotNull DialogBuilder builder, @NotNull final TwitterDialogPanel panel) {
private static DocumentListener createTextFieldLengthDocumentListener(@NotNull TwitterDialogWrapper builder, @NotNull final StudyTwitterUtils.TwitterDialogPanel panel) {
return new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
int length = e.getDocument().getLength();
if (length > 140 || length == 0) {
builder.setOkActionEnabled(false);
builder.setOKActionEnabled(false);
panel.getRemainSymbolsLabel().setText("<html><font color='red'>" + String.valueOf(140 - length) + "</font></html>");
}
else {
builder.setOkActionEnabled(true);
} else {
builder.setOKActionEnabled(true);
panel.getRemainSymbolsLabel().setText(String.valueOf(140 - length));
}
}
};
}
/**
* Dialog wrapper class with DoNotAsl option for asking user to tweet.
* */
private static class TwitterDialogWrapper extends DialogWrapper {
private StudyTwitterUtils.TwitterDialogPanel myPanel;
TwitterDialogWrapper(@Nullable Project project, @NotNull StudyTwitterUtils.TwitterDialogPanel panel, DoNotAskOption doNotAskOption) {
super(project);
setTitle("Twitter");
setDoNotAskOption(doNotAskOption);
setOKButtonText("Tweet");
setCancelButtonText("No");
setResizable(true);
Dimension preferredSize = panel.getPreferredSize();
setSize((int) preferredSize.getHeight(), (int) preferredSize.getWidth());
myPanel = panel;
init();
}
public void setOKActionEnabled(boolean isEnabled) {
super.setOKActionEnabled(isEnabled);
}
@Nullable
@Override
protected JComponent createCenterPanel() {
return new JBScrollPane(myPanel);
}
}
/**
* Class provides structure for twitter dialog panel
*/
@@ -186,25 +275,23 @@ public class StudyTwitterUtils {
/**
* Provides tweet text
* @return
*/
@NotNull public abstract String getMessage();
/**
*
* @return Input stream of media should be posted or null if there's nothing to post
*/
@Nullable public abstract InputStream getMediaSource();
@Nullable public abstract String getMediaExtension();
/**
*
* @return label that will be used to show remained symbol number
*/
@NotNull public abstract JLabel getRemainSymbolsLabel();
/**
* Api to add document listener to field containing tweet text
* @param documentListener
*/
public abstract void addTextFieldVerifier(@NotNull final DocumentListener documentListener);