Connect in Apex Pilot

Source code:

Overview

The Connect in Apex pilot provides Chatter REST API functionality inside Apex. Use Connect in Apex to build user interfaces that include Chatter resources such as feeds, groups, users, followers, likes, and comments. (Files, recommendations, topics, and private messages are not included in the pilot, but are expected at a later date.) The primary benefit of Connect in Apex is that developers building Chatter integrations and custom chatter UI on Force.com no longer need to perform an Apex callout to the HTTP API to use the Chatter REST API. The data is available as a method on the new ConnectAPI namespace inside apex. Connect in Apex provides a simple programming model and more complete data for building Chatter UI, including getting and posting @mentions and likes.

To enroll in the Connect in Apex pilot, contact Gregg Johnson (gregg.johnson@salesforce.com).

View Helper Classes

To massage the output to display exactly the way you want it to in VisualForce, create view helper classes. [code apex] global class FeedFormatter { global static String formatBodyText(ConnectApi.FeedBody body) { String formattedText = ''; for (ConnectApi.MessageSegment seg : body.messageSegments) { if (seg instanceof ConnectApi.MentionSegment) { ConnectApi.MentionSegment mention = (ConnectApi.MentionSegment)seg; formattedText += '' + mention.user.name + ''; } else if (seg instanceof ConnectApi.HashtagSegment) { ConnectApi.HashtagSegment hashtag = (ConnectApi.HashtagSegment)seg; formattedText += '#' + hashtag.tag + ''; } else if (seg instanceof ConnectApi.LinkSegment) { ConnectApi.LinkSegment link = (ConnectApi.LinkSegment)seg; formattedText += '' + link.url + ''; } else { // Default. formattedText += seg.text; } } return formattedText; } } [/code] [code apex] global class CommentInfo { global CommentInfo(ConnectApi.Comment inComment) { comment = inComment; userName = inComment.user.name; imageUrl = ''; if (inComment.attachment != null && inComment.attachment instanceof ConnectApi.ContentAttachment) { ConnectApi.ContentAttachment content = (ConnectApi.ContentAttachment)inComment.attachment; imageUrl = content.renditionUrl; } formattedText = FeedFormatter.formatBodyText(inComment.body); } global ConnectApi.Comment comment { get; private set; } global String userName { get; private set; } global String imageUrl { get; private set; } global String formattedText { get; private set; } } [/code] The FeedItemInfo class uses the FeedFormatter and CommentInfo classes and assembles the Apex output to display @mentions, hashtags, embedded links and other rich text in HTML. [code apex] global class FeedItemInfo { global FeedItemInfo(ConnectApi.FeedItem inFeedItem) { feedItem = inFeedItem; userName = ''; if (inFeedItem.actor != null && inFeedItem.actor instanceof ConnectApi.UserSummary) { userName = ((ConnectApi.UserSummary)inFeedItem.actor).name; } imageUrl = ''; if (inFeedItem.attachment != null) { if (inFeedItem.attachment instanceof ConnectApi.ContentAttachment) { ConnectApi.ContentAttachment content = (ConnectApi.ContentAttachment)inFeedItem.attachment; imageUrl = content.renditionUrl; contentDescription = content.description; contentTitle = content.title; hasImagePreview = content.hasImagePreview; contentId = content.versionId; // contentDownloadUrl = content.downloadUrl; // not cookie enabled so unusable contentDownloadUrl = '/sfc/servlet.shepherd/version/download/' + content.versionId + '?asPdf=false&operationContext=CHATTER'; } else if (inFeedItem.attachment instanceof ConnectApi.LinkAttachment) { ConnectApi.LinkAttachment link = (ConnectApi.LinkAttachment)inFeedItem.attachment; linkUrl = link.url; linkTitle = link.title; } } formattedText= FeedFormatter.formatBodyText(inFeedItem.body); comments = new List(); for (ConnectApi.Comment comment : inFeedItem.comments.comments) { comments.add(new CommentInfo(comment)); } } //-------- properties ----------// global ConnectApi.FeedItem feedItem { get; private set; } global String userName { get; private set; } global String imageUrl { get; private set; } global String linkUrl { get; private set; } global String linkTitle { get; private set; } global String contentDescription { get; private set; } global String contentDownloadUrl { get; private set; } global String contentTitle { get; private set; } global Boolean hasImagePreview { get; private set; } global String formattedText { get; private set; } global String contentId { get; private set; } global List comments { get; private set; } global Integer commentCount { get { return feedItem.comments.comments.size(); } } } [/code]

Getting a Feed

Getting a feed is simple. Use the getFeedItemsFromFeed static method to access pages of different feeds. Specify the feed type in the second parameter. The method also takes additional parameters such as a page number, page size, and sort order.

If you are part of the [url http://www.salesforce.com/chatter/communities/faq/]Communities pilot[/url] you can choose the target community by passing in a non-null value for the first parameter.

The DemoController uses the FeedItemInfo view helper class we created earlier to format the Chatter data.

[code apex] global class DemoController { // get first page of news feed global ConnectApi.FeedItemPage getNewsFeed() { return ConnectApi.ChatterFeeds.getFeedItemsFromFeed(null, ConnectApi.FeedType.News, 'me'); } // build list of wrapped feed items for display in VisualForce global List getNewsFeedForDisplay() { ConnectApi.FeedItemPage feed = getNewsFeed(); List result = new List(); for (ConnectApi.FeedItem item : feed.items) { result.add(new FeedItemInfo(item)); } return result; } } [/code]

VisualForce for Displaying a Feed

Here's some example VisualForce markup that uses the DemoController to display feed items. [code apex]
{!commentInfo.userName}
[/code]

Posting to a Feed

To post a feed item, build an instance of ConnectApi.FeedItemInput and set its body to an instance of MessageBodyInput. MessageBodyInput contains the data necessary to post @mentions and other rich text.

This code parses out @-mentions from user-provided text and converts the text into a ConnectApi.FeedItemInput that's ready to post.

[code apex] public class FeedBodyParser { // use to extract the user id as a group // Put in double backslashes into regex because Apex doesn't recognize that its a regex // actual regex is @\[[^\]]{4,}\]\(contact:(\w{15,18})\) public static final String MENTIONGROUPPATTERN = '@\\[[^\\]]{4,}\\]\\(user:(\\w{15,18})\\)'; // use to extract the entire mention format public static final String MENTIONPATTERN = '@\\[[^\\]]{4,}\\]\\(user:\\w{15,18}\\)'; public class postTooLargeException extends Exception {} // used to store mentions extracted from the post public class Mention { public Integer groupStart { get; set; } public Integer groupEnd { get; set; } public String text { get; set; } } // parse post text into a list of Mentions public static List extractAllMentions(String post) { Pattern pat = Pattern.compile(MENTIONPATTERN); Matcher matcher = pat.matcher(post); List mentions = new List(); while(matcher.find()) { Mention mention = new Mention(); mention.text = matcher.group(); // text of matched mention mention.groupStart = matcher.start(); // position in string where mention starts mention.groupEnd = matcher.end() ; // position after the last char of the mention mentions.add(mention); } return mentions; } // parse the post body, placing @mentions and text into the passed in // segments list. Works with both feed items and comments - just pass in the // segments that are assigned to the appropriate object. public static void buildSegments(String postText, List segments) { List mentions = extractAllMentions(postText); if (mentions.size() > 0) { System.debug('mention[0]' + mentions[0]); // the cursor is the char position inside post Integer cursor = 0; for (Mention mention: mentions) { // cursor is maintained at the beginning of a mention or text segment by moving it one char past the current // mention at every iteration of this loop. if (mention.groupStart > cursor) { // there is text between where the cursor is and the start of this mention so store the text first. // we know there is text (and we're not at end of string) because there's a mention with a start point to the right // of the cursor. ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput(); textSegment.text = postText.subString(cursor, mention.groupStart); segments.add(textSegment); } // next, store the mention ConnectApi.MentionSegmentInput mentionSegment = new ConnectApi.MentionSegmentInput(); String temptxt = parseOneMention(mention.text); mentionSegment.id = parseOneMention(mention.text); segments.add(mentionSegment); cursor = mention.groupEnd; // move cursor 1 char past where this mention ended } // After the last mention, there may be a text segment if (cursor < postText.length()) { ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput(); textSegment.text = postText.subString(cursor, postText.length()); segments.add(textSegment); } } else { // no mentions in the post, just store the whole post as a text segment. ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput(); textSegment.text = postText; segments.add(textSegment); } } // parse the user id out of a string that has one mention in it and return it. public static String parseOneMention(String mentionStr) { Pattern pat = Pattern.compile(MENTIONGROUPPATTERN); Matcher matcher = pat.matcher(mentionStr); matcher.find(); return matcher.group(1); } // returns a corresponding FeedItemInput which can be used to post a new feed item public static ConnectApi.FeedItemInput convertToFeedItemInput(String postText) { // failsafe - postText size should be controlled by browser. if (postText.length() > 2000) { throw new postTooLargeException('post too large'); } ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput(); feedItemInput.body = new ConnectApi.MessageBodyInput(); buildSegments(postText, feedItemInput.body.messageSegments); return feedItemInput; } } [/code] Use the following code with a VisualForce page that makes use of jquery-mentions formatting for the text that gets passed in as feedItemText for each of the methods. [code apex] global class FeedItemPoster { global static ConnectApi.FeedItem postTextFeedItem(String feedItemText) { ConnectApi.FeedItemInput feedItemInput = FeedBodyParser.convertToFeedItemInput(feedItemText); return ConnectApi.ChatterFeeds.postFeedItem(null, ConnectApi.FeedType.News, 'me', feedItemInput, null); } global static ConnectApi.FeedItem postFileFeedItem(String feedItemText, Blob fileBlob, String title, String description, String filename) { ConnectApi.FeedItemInput feedItemInput = FeedBodyParser.convertToFeedItemInput(feedItemText); // file attachment ConnectApi.NewFileAttachmentInput fileIn = new ConnectApi.NewFileAttachmentInput(); fileIn.title = title; // user-given "name" is set using the title fileIn.description = description; feedItemInput.attachment = fileIn; ConnectApi.BinaryInput feedBinary = new ConnectApi.BinaryInput(fileBlob, null, filename); return ConnectApi.ChatterFeeds.postFeedItem(null, ConnectApi.FeedType.News, 'me', feedItemInput, feedBinary); } global ConnectApi.FeedItem postLinkFeedItem(String feedItemText, String linkName, String linkUrl) { ConnectApi.FeedItemInput feedItemInput = FeedBodyParser.convertToFeedItemInput(feedItemText); ConnectApi.LinkAttachmentInput linkIn = new ConnectApi.LinkAttachmentInput(); linkIn.urlName = linkName; linkIn.url = linkUrl; feedItemInput.attachment = linkIn; return ConnectApi.ChatterFeeds.postFeedItem(null, ConnectApi.FeedType.News, 'me', feedItemInput, null); } } [/code]