Commits

Shihab Hamid  committed 623ae46

inline actions in place

  • Participants
  • Parent commits 27157fd

Comments (0)

Files changed (13)

-You have successfully created a plugin using the Confluence plugin archetype!
+Bring social media monitoring to Confluence!
 
-Here are the SDK commands you'll use immediately:
+This is an example integration of Twitter with Confluence's WorkBox.
 
-* atlas-run   -- installs this plugin into Confluence and starts it on http://<machinename>:1990/confluence
-* atlas-debug -- same as atlas-run, but allows a debugger to attach at port 5005
-* atlas-cli   -- after atlas-run or atlas-debug, opens a Maven command line window:
-                 - 'pi' reinstalls the plugin into the running Confluence instance
-* atlas-help  -- prints description for all commands in the SDK
+You will need to add some Twitter OAuth credentials in /src/main/resources/twitter4j.properties to be able to use the Twitter APIs.
 
-Full documentation is always available at:
+To fire this up, run mvn confluence:run from the command line (or atlas-run if you have the Atlassian Plugin SDK installed).
+
+The Confluence instance will be up at http://localhost:1990/confluence
+
+For more information on the plugin SDK, check out:
 
 https://developer.atlassian.com/display/DOCS/Developing+with+the+Atlassian+Plugin+SDK
             <artifactId>twitter4j-stream</artifactId>
             <version>[2.2,)</version>
         </dependency>
+
+        <!-- json libs -->
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+            <version>1.4.4</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+            <version>1.4.4</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
     <build>

File src/main/java/com/atlascamp/mywork/tweets/Main.java

-package com.atlascamp.mywork.tweets;
-
-import twitter4j.*;
-
-import java.io.IOException;
-
-public class Main
-{
-    public static void main(String[] args) throws TwitterException, IOException
-    {
-        StatusListener listener = new StatusListener(){
-            public void onStatus(Status status) {
-                System.out.println(status.getUser().getName() + " : " + status.getText());
-            }
-            public void onDeletionNotice(StatusDeletionNotice statusDeletionNotice) {}
-            public void onTrackLimitationNotice(int numberOfLimitedStatuses) {}
-
-            @Override
-            public void onScrubGeo(long l, long l1)
-            {
-                throw new RuntimeException();
-            }
-
-            public void onException(Exception ex) {
-                ex.printStackTrace();
-            }
-        };
-        TwitterStream twitterStream = new TwitterStreamFactory().getInstance();
-        twitterStream.addListener(listener);
-        // sample() method internally creates a thread which manipulates TwitterStream and calls these adequate listener methods continuously.
-        twitterStream.sample();
-    }
-}

File src/main/java/com/atlascamp/mywork/tweets/TweetsActionService.java

+package com.atlascamp.mywork.tweets;
+
+import com.atlassian.mywork.service.ActionResult;
+import com.atlassian.mywork.service.ActionService;
+import org.apache.commons.lang.StringUtils;
+import org.codehaus.jackson.JsonNode;
+import twitter4j.StatusUpdate;
+import twitter4j.Twitter;
+import twitter4j.TwitterException;
+import twitter4j.TwitterFactory;
+
+/**
+ * Allows users to retweet or reply to tweets.
+ */
+public class TweetsActionService implements ActionService
+{
+    @Override
+    public String getApplication()
+    {
+        return new TweetsRegistrationProvider().getApplication();
+    }
+
+    @Override
+    public ActionResult execute(String username, JsonNode action)
+    {
+        Twitter twitter = new TwitterFactory().getInstance();
+
+        String globalId = action.path("globalId").getValueAsText();
+        long tweetId = Long.parseLong(globalId.replace("tweet:", ""));
+
+        String qualifiedAction = action.path("qualifiedAction").getValueAsText();
+        String entityAction = qualifiedAction.replace(getApplication() + ".tweet.", "");
+
+        if (entityAction.equals("retweet"))
+        {
+            try
+            {
+                twitter.retweetStatus(tweetId);
+                return ActionResult.SUCCESS;
+            }
+            catch (TwitterException e)
+            {
+                e.printStackTrace();
+                return ActionResult.FAILED;
+            }
+        }
+        else if (entityAction.equals("reply"))
+        {
+            String replyTweet = action.path("comment").getValueAsText();
+            String userHandle = action.path("metadata").path("user").getValueAsText();
+
+            String fullReply = userHandle + " " + replyTweet;
+            fullReply = StringUtils.abbreviate(fullReply, 140);
+
+            try
+            {
+                twitter.updateStatus(new StatusUpdate(fullReply).inReplyToStatusId(tweetId));
+                return ActionResult.SUCCESS;
+            }
+            catch (TwitterException e)
+            {
+                e.printStackTrace();
+                return ActionResult.FAILED;
+            }
+        }
+
+        // unmapped action
+        return ActionResult.FAILED;
+    }
+}

File src/main/java/com/atlascamp/mywork/tweets/TweetsRegistrationProvider.java

 import com.atlassian.mywork.service.RegistrationProvider;
 
 /**
- * Let's WorkBox know where to find the registration configuration
- * details like i18n and in-line actions.
+ * Lets WorkBox know where to find the registration configuration
+ * details like i18n and in-line actions associated with our
+ * notifications.
  */
 public class TweetsRegistrationProvider implements RegistrationProvider
 {

File src/main/java/com/atlascamp/mywork/tweets/TwitterListener.java

 import com.atlassian.crowd.search.builder.QueryBuilder;
 import com.atlassian.crowd.search.query.entity.EntityQuery;
 import com.atlassian.event.api.EventPublisher;
+import com.atlassian.mywork.model.Notification;
 import com.atlassian.mywork.model.NotificationBuilder;
 import com.atlassian.mywork.service.NotificationService;
+import org.codehaus.jackson.node.JsonNodeFactory;
+import org.codehaus.jackson.node.ObjectNode;
 import org.springframework.beans.factory.DisposableBean;
 import twitter4j.*;
 import twitter4j.conf.ConfigurationBuilder;
 public class TwitterListener implements StatusListener, DisposableBean
 {
     // configuration
-    private static final String USERNAME = "atlascampbob";
-    private static final String PASSWORD = "";
-    private static final String GROUP = "marketing";
-    private static final String[] SEARCH = { "test :)" };
+    private static final String GROUP_TO_NOTIFY = "marketing";
+    private static final String[] KEYWORDS = { "test :)" };
 
     // dependencies
     private final NotificationService notificationService;
     private final CrowdService crowdService;
     private final TwitterStream twitterStream;
 
-    public TwitterListener(EventPublisher eventPublisher, NotificationService notificationService, CrowdService crowdService)
+    public TwitterListener(NotificationService notificationService, CrowdService crowdService)
     {
         // inject dependencies
         this.notificationService = notificationService;
         this.crowdService = crowdService;
 
         // configure twitter
-        ConfigurationBuilder configBuilder = new ConfigurationBuilder().setUser(USERNAME).setPassword(PASSWORD);
-        this.twitterStream = new TwitterStreamFactory(configBuilder.build()).getInstance();
+        this.twitterStream = new TwitterStreamFactory().getInstance();
 
         // start streaming
         twitterStream.addListener(this);
-        twitterStream.filter(new FilterQuery().track(SEARCH));
+        twitterStream.filter(new FilterQuery().track(KEYWORDS));
     }
 
     public void onStatus(Status status)
     {
         System.out.println(status.getUser().getName() + " : " + status.getText());
 
-        notificationService.createOrUpdate("admin", new NotificationBuilder()
-                            .application("com.atlascamp.mywork.tweets")
-                            .entity("twitter")
-                            .action("tweet")
-                            .title(status.getUser().getName())
-                            .description(status.getText())
-                            .createNotification());
+        NotificationBuilder builder = new NotificationBuilder()
+                .application("com.atlascamp.mywork.tweets")
+                .entity("tweet")
+                .action("post")
+                .description(status.getText())
+                .itemUrl("http://twitter.com/" + status.getUser().getScreenName() + "/status/" + status.getId())
+                .itemIconUrl("https://twitter.com/images/resources/twitter-bird-16x16.png")
+                .iconUrl(status.getUser().getProfileImageURL().toString())
+                .metadata(createMetadata("user", "@" + status.getUser().getScreenName()));
+
+        // is it an original tweet or a re-tweet?
+        if (status.getRetweetedStatus() != null)
+        {
+            builder.globalId("tweet:" + status.getRetweetedStatus().getId());
+            builder.title(status.getRetweetedStatus().getText());
+        }
+        else
+        {
+            builder.globalId("tweet:" + status.getId());
+            builder.title(status.getText());
+        }
+
+        Notification notification = builder.createNotification();
+
+        // broadcast the notification to the group
+        for (String user : getUsersInGroup(GROUP_TO_NOTIFY))
+        {
+            notificationService.createOrUpdate(user, notification);
+        }
+
+        // also send it to the admin user so we can easily test this
+        notificationService.createOrUpdate("admin", notification);
+    }
+
+    private ObjectNode createMetadata(String key, String val)
+    {
+        ObjectNode metadata = JsonNodeFactory.instance.objectNode();
+        metadata.put(key, val);
+        return metadata;
     }
 
     @Override

File src/main/resources/atlassian-plugin.xml

         <vendor name="${project.organization.name}" url="${project.organization.url}" />
     </plugin-info>
 
+    <!-- listens for tweets and produces notifications -->
     <component key="twitterListener" class="com.atlascamp.mywork.tweets.TwitterListener"/>
+
+    <!-- defines what can be done with tweet notifications -->
     <component key="registrationProvider" class="com.atlascamp.mywork.tweets.TweetsRegistrationProvider" public="true">
         <interface>com.atlassian.mywork.service.RegistrationProvider</interface>
     </component>
+    <component key="tweetsActionService" class="com.atlascamp.mywork.tweets.TweetsActionService" public="true">
+        <interface>com.atlassian.mywork.service.ActionService</interface>
+    </component>
+    <resource type="i18n" name="twitter-i18n" location="com.atlascamp.mywork.tweets.registration-i18n"/>
 
+    <!-- dependencies -->
     <component-import key="eventPublisher" interface="com.atlassian.event.api.EventPublisher"/>
     <component-import key="crowdService" interface="com.atlassian.crowd.embedded.api.CrowdService"/>
     <component-import key="notificationService" interface="com.atlassian.mywork.service.NotificationService"/>
-    <!--<component-import key="registrationProvider" interface="com.atlassian.mywork.service.RegistrationProvider"/>-->
+
 
 </atlassian-plugin>

File src/main/resources/com/atlascamp/mywork/tweets/registration-actions.json

+{
+    "openLink": {
+        "type": "link",
+        "objectActions": ["tweet"]
+    },
+    "retweet": {
+        "objectActions": ["tweet"],
+        "type": "ajax",
+        "attrs": {
+            "instant": true
+        },
+        "data": []
+    },
+    "reply": {
+        "actions": ["tweet.post"],
+        "type": "form",
+        "data": [
+            {
+                "type": "textarea",
+                "name": "comment"
+            }
+        ]
+    }
+}
+

File src/main/resources/com/atlascamp/mywork/tweets/registration-i18n.properties

-com.atlascamp.mywork.tweets.twitter.tweet.title={user} tweeted {title}
-com.atlascamp.mywork.tweets.twitter.tweet.aggregate={0} tweet on {1}
-com.atlascamp.mywork.tweets.twitter.tweet.aggregates={0} tweets on {1}
-com.atlascamp.mywork.tweets.twitter.tweet.aggregatenew={0} new tweet on {1}
-com.atlascamp.mywork.tweets.twitter.tweet.aggregatenews={0} new tweets on {1}
-com.atlascamp.mywork.tweets.twitter.tweet.older={0} older tweets on this tweet
-com.atlascamp.mywork.tweets.twitter.tweet.olders={0} older tweets on this tweet
+com.atlascamp.mywork.tweets.tweet.post.title={user} tweeted {title}
+com.atlascamp.mywork.tweets.tweet.post.aggregate={0} post on {1}
+com.atlascamp.mywork.tweets.tweet.post.aggregates={0} posts on {1}
+com.atlascamp.mywork.tweets.tweet.post.aggregatenew={0} new post on {1}
+com.atlascamp.mywork.tweets.tweet.post.aggregatenews={0} new posts on {1}
+com.atlascamp.mywork.tweets.tweet.post.older={0} older post on this tweet
+com.atlascamp.mywork.tweets.tweet.post.olders={0} older posts on this tweet
+
+com.atlascamp.mywork.tweets.action.openLink.displayName=Open
+
+com.atlascamp.mywork.tweets.action.reply.displayName=Reply
+com.atlascamp.mywork.tweets.action.reply.title=Post a reply to this tweet
+com.atlascamp.mywork.tweets.action.reply.submitLabel=Reply
+com.atlascamp.mywork.tweets.action.reply.alternateDisplayName=Replied
+
+com.atlascamp.mywork.tweets.action.retweet.displayName=Retweet
+com.atlascamp.mywork.tweets.action.retweet.title=Retweet this post
+com.atlascamp.mywork.tweets.action.retweet.alternateDisplayName=You retweeted this

File src/main/resources/twitter4j.properties

+# to create an oauth token, see https://dev.twitter.com/docs/auth/tokens-devtwittercom
+oauth.consumerKey=*
+oauth.consumerSecret=*
+oauth.accessToken=*
+oauth.accessTokenSecret=*

File src/test/java/com/atlascamp/mywork/tweets/ExampleMacroTest.java

-package com.atlascamp.mywork.tweets;
-
-import org.junit.Test;
-
-/**
- * Testing {@link com.atlascamp.mywork.tweets.ExampleMacro}
- */
-public class ExampleMacroTest
-{
-    @Test
-    public void basic()
-    {
-        // add test here...
-    }
-}

File src/test/java/it/AbstractIntegrationTestCase.java

-package it;
-
-import com.atlassian.confluence.plugin.functest.AbstractConfluencePluginWebTestCase;
-import com.atlassian.confluence.plugin.functest.JWebUnitConfluenceWebTester;
-import com.atlassian.confluence.plugin.functest.TesterConfiguration;
-import junit.framework.Assert;
-
-import java.io.IOException;
-import java.util.Properties;
-
-public class AbstractIntegrationTestCase extends AbstractConfluencePluginWebTestCase
-{
-    @Override
-    protected JWebUnitConfluenceWebTester createConfluenceWebTester()
-    {
-        Properties props = new Properties();
-        props.put("confluence.webapp.protocol", "http");
-        props.put("confluence.webapp.host", "localhost");
-
-        // this is deceiving: the func test library checks for the system properties
-        // *before* checking in this properties file for these values, so these
-        // properties are technically ignored
-        props.put("confluence.webapp.port", Integer.parseInt(System.getProperty("http.port")));
-        props.put("confluence.webapp.context.path", System.getProperty("context.path"));
-
-        props.put("confluence.auth.admin.username", "admin");
-        props.put("confluence.auth.admin.password", "admin");
-
-        TesterConfiguration conf;
-        try
-        {
-            conf = new TesterConfiguration(props);
-        }
-        catch (IOException ioe)
-        {
-            Assert.fail("Unable to create tester: " + ioe.getMessage());
-            return null;
-        }
-
-        JWebUnitConfluenceWebTester tester = new JWebUnitConfluenceWebTester(conf);
-
-        tester.getTestContext().setBaseUrl(tester.getBaseUrl());
-        tester.setScriptingEnabled(false);
-
-        return tester;
-    }
-}

File src/test/java/it/IntegrationTestMyPlugin.java

-package it;
-
-public class IntegrationTestMyPlugin extends AbstractIntegrationTestCase
-{
-	public void testSomething()
-	{
-        gotoPage("");
-        assertTextPresent("Welcome");
-	}
-}