├── .gitignore ├── LICENSE ├── README.md ├── images ├── chatter-bot-getting-started-video.png ├── chatter-bot-post-message-process-builder.png ├── chatter-bot-post-message.png └── impersonate-user-with-care-superpower.png └── src ├── classes ├── ChatterBotPostMessageEmailHandler.cls ├── ChatterBotPostMessageEmailHandler.cls-meta.xml ├── ChatterBotPostMessageEmailHandlerTest.cls ├── ChatterBotPostMessageEmailHandlerTest.cls-meta.xml ├── ChatterBotPostMessageInvocable.cls ├── ChatterBotPostMessageInvocable.cls-meta.xml ├── ChatterBotPostMessageInvocableTest.cls ├── ChatterBotPostMessageInvocableTest.cls-meta.xml ├── ChatterBotPostMessageService.cls ├── ChatterBotPostMessageService.cls-meta.xml ├── ChatterBotPostMessageServiceTest.cls ├── ChatterBotPostMessageServiceTest.cls-meta.xml ├── ConnectApiHelper.cls ├── ConnectApiHelper.cls-meta.xml ├── ConnectApiHelperTest.cls └── ConnectApiHelperTest.cls-meta.xml ├── email ├── Chatter_Bot_Email_Templates-meta.xml └── Chatter_Bot_Email_Templates │ ├── Chatter_Bot_Post_Message_Template.email │ └── Chatter_Bot_Post_Message_Template.email-meta.xml ├── flowDefinitions └── Chatter_Bot_Welcome_New_Group_Member.flowDefinition ├── flows └── Chatter_Bot_Welcome_New_Group_Member-6.flow ├── objects └── Chatter_Bot_Feeds_Setting__c.object ├── package.xml └── permissionsets └── Chatter_Bot_Feeds_Admin.permissionset /.gitignore: -------------------------------------------------------------------------------- 1 | config/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Doug Ayers 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Salesforce Chatter Bot for Feeds 2 | ================================ 3 | 4 | ![image](/images/chatter-bot-post-message.png) 5 | 6 | Overview 7 | -------- 8 | 9 | Chatter Bot for Feeds enables you to post Chatter messages authored by any user. The best part is that you can automate these messages with Process Builder or Flow! 10 | Although there are many ways to post Chatter messages in Salesforce using Process Builder, Flow, Apex, or API, no one approach satisfied all of my requirements: 11 | 12 | 1. Must be easy to use and launchable by Process Builder or Flow (declarative) 13 | 2. Must support setting the author to someone other than the running user 14 | 3. Must support rich-text 15 | 4. Must support @ mentions 16 | 17 | To learn more about my design decisions, please [read my blog post](https://douglascayers.com/2017/01/15/chatter-bot-for-feeds/) introducing Chatter Bot for Feeds. 18 | 19 | 20 | Installation & Getting Started 21 | ------------------------------ 22 | 23 | *Video Tutorial* 24 | [![getting-started-video](/images/chatter-bot-getting-started-video.png)](https://www.youtube.com/watch?v=5Ls-S8DS8Vs "Video Tutorial") 25 | 26 | 1. Follow the **Getting Started** steps and deploy [Chatter Bot for Groups](https://github.com/DouglasCAyers/salesforce-chatter-bot-groups#overview). *(the examples depend on this)* 27 | 2. Deploy [Chatter Bot for Feeds](https://githubsfdeploy.herokuapp.com/). 28 | 3. In Setup, create an **Email Service** using apex class `ChatterBotPostMessageEmailHandler` then create an **Email Address**. Set the **Context User** for the email service address to an **administrator** user. Take note of the generated email address as we will use it in a future step. 29 | 4. Assign the **Chatter Bot Feeds Admin** permission set to the **Context User** you chose. This allows setting the author for new Chatter posts to someone other than the current user. 30 | 5. Create a [Chatter Free](https://help.salesforce.com/articleView?id=users_license_types_chatter.htm&type=0&language=en_US) user and set their **email address** to be the same as the Email Service Address from Step 3. 31 | 6. Create a default organization default value for the **Chatter Bot Feeds Setting** custom setting and copy into the **Email Service Address User ID** field the ID of the Chatter Free user from Step 5. 32 | 7. To test, add yourself or another user to a Chatter Group monitored by **Chatter Bot for Groups** (see Step 1). Within a couple seconds you should be able to refresh that group's feed and see the welcome post. Congratulations! 33 | 34 | 35 | Next Steps 36 | ========== 37 | 38 | Now that you have all the configuration in place, now it's time to focus on your actual use cases for automating Chatter posts and deciding **who** you want the author to be in each of those scenarios. 39 | 40 | 41 | Email Templates: Fancy Messages 42 | ------------------------------- 43 | 44 | Refer to the **Chatter Bot Post Message Template** email template for examples of supported rich-text in Chatter posts and the syntax for @ mentions. Your messages can also include merge fields from a record identified by the variable **Record ID (Template Merge Fields)** when invoking the **CB: Post Message** Apex method in Process Builder. 45 | 46 | Alternatively, rather than specify values for `Email Template Unique Name` and `Record ID( Template Merge Fields` variables, you can use the `Message` variable to specify your Chatter message right within Process Builder. Sometimes that is an easy option for simple messages. 47 | 48 | 49 | Process Builder 50 | --------------- 51 | 52 | Refer to the example process named **Chatter Bot - Welcome New Group Member**. There is one Immediate Action that demonstrates how to invoke the **CB: Post Message** Apex method. By default, the **Author User ID** is set the `$User.Id`, the current user who causes the process to fire. For your purposes, you will want to change that to your desired Chatter post author such as a community manager, the CEO, a comical Chatter Free user, whatever. 53 | 54 | ![image](/images/chatter-bot-post-message-process-builder.png) 55 | 56 | 57 | Responsibility 58 | ============== 59 | 60 | As the [#AwesomeAdmin](https://twitter.com/hashtag/awesomeadmin) that you are, you understand that with [great power comes great responsibility](http://www.slideshare.net/Salesforce/appexchange-super-hero-3). Impersonate other users on Chatter with care and always have consent from the intended author before automating posts by them. Thanks! 61 | 62 | ![image](/images/impersonate-user-with-care-superpower.png) 63 | 64 | 65 | FAQ 66 | === 67 | 68 | Why do I get error "The object named Chatter_Bot_Group_Member__c can't be found." during deployment? 69 | ---------------------------------------------------------------------------------------------------- 70 | 71 | The example Process Builder included in **Chatter Bot for Feeds** is designed for the use case when users join a group. That capability is only provided through **Chatter Bot for Groups**. 72 | See also question [Can Chatter Bot for Feeds be used without Chatter Bot for Groups?](#can-chatter-bot-for-feeds-be-used-without-chatter-bot-for-groups). 73 | 74 | 75 | Can Chatter Bot for Feeds be used without Chatter Bot for Groups? 76 | ----------------------------------------------------------------- 77 | 78 | In practice, yes. For deployment, not at this time. The example Process Builder included in **Chatter Bot for Feeds** is designed for the use case when users join a group. That capability is only provided through **Chatter Bot for Groups**. 79 | Once deployed, you can begin automating Chatter posts for any reason you want using Process Builder or Flow. The dependency on **Chatter Bot for Groups** is just for the example Process Builder. 80 | 81 | 82 | Why do I need an Email Service to make Chatter posts? 83 | ----------------------------------------------------- 84 | 85 | The Email Service is only so that we can get code to execute as an administrator and not the current user. Technically, you don't need it to post Chatter messages unless you have these three requirements: 86 | 87 | 1. You want to set the author to someone other than the running user 88 | 2. You want to post a rich-text message 89 | 3. You want to @ mention users or groups 90 | 91 | To learn more about my design decisions, please [read my blog post](https://douglascayers.com/2017/01/15/chatter-bot-for-feeds/) introducing Chatter Bot for Feeds. 92 | 93 | 94 | Why is a Chatter Free user necessary for the Email Service? 95 | ----------------------------------------------------------- 96 | 97 | Not for any technical reason to get this solution to work but rather to avoid using up any of your [org's daily email quota](https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_email.htm). 98 | Apex can send a certain number of emails to external addresses each day, but an unlimited number of emails can be sent to internal users. Using a Chatter Free user for this purpose is a free and easy way around the Apex limitation. 99 | 100 | 101 | Where will the Chatter post be made to? 102 | --------------------------------------- 103 | 104 | You can specify a User ID, Chatter Group ID, or record ID where you would like the Chatter post to be made. If you specify a FeedItem ID (a Chatter post ID) then a [FeedComment](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_feedcomment.htm) will be created for that Chatter conversation instead of a whole new Chatter post. 105 | -------------------------------------------------------------------------------- /images/chatter-bot-getting-started-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglascayers/salesforce-chatter-bot-feeds/ec1b9ba56f2fab89cfbefb6311719eef3abbb3ef/images/chatter-bot-getting-started-video.png -------------------------------------------------------------------------------- /images/chatter-bot-post-message-process-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglascayers/salesforce-chatter-bot-feeds/ec1b9ba56f2fab89cfbefb6311719eef3abbb3ef/images/chatter-bot-post-message-process-builder.png -------------------------------------------------------------------------------- /images/chatter-bot-post-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglascayers/salesforce-chatter-bot-feeds/ec1b9ba56f2fab89cfbefb6311719eef3abbb3ef/images/chatter-bot-post-message.png -------------------------------------------------------------------------------- /images/impersonate-user-with-care-superpower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglascayers/salesforce-chatter-bot-feeds/ec1b9ba56f2fab89cfbefb6311719eef3abbb3ef/images/impersonate-user-with-care-superpower.png -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageEmailHandler.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Developed by Doug Ayers (douglascayers.com) 3 | * https://github.com/DouglasCAyers/salesforce-chatter-bot-feeds 4 | * 5 | * https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_ConnectAPI_ChatterFeeds_static_methods.htm#apex_ConnectAPI_ChatterFeeds_postFeedElement_3 6 | * https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/connectapi_examples_post_feed_element_mention.htm 7 | * https://github.com/forcedotcom/ConnectApiHelper 8 | */ 9 | public with sharing class ChatterBotPostMessageEmailHandler implements Messaging.InboundEmailHandler { 10 | 11 | public Messaging.InboundEmailResult handleInboundEmail( Messaging.InboundEmail email, Messaging.InboundEnvelope envelope ) { 12 | 13 | Messaging.InboundEmailResult result = new Messaging.InboundEmailResult(); 14 | 15 | SavePoint sp = Database.setSavePoint(); 16 | 17 | try { 18 | 19 | System.debug( 'Handling inbound email: ' + email ); 20 | System.debug( envelope ); 21 | 22 | processEmail( email ); 23 | 24 | // if result is false then salesforce does not commit DML changes 25 | result.success = true; 26 | 27 | } catch ( Exception e ) { 28 | 29 | System.debug( LoggingLevel.ERROR, e.getMessage() + ' : ' + e.getStackTraceString() ); 30 | 31 | result.message = e.getMessage() + '\n' + e.getStackTraceString(); 32 | result.success = false; 33 | 34 | } 35 | 36 | if ( result.success == false ) { 37 | if ( sp != null ) { 38 | System.debug( LoggingLevel.ERROR, 'Rolling back transaction' ); 39 | Database.rollback( sp ); 40 | } 41 | } 42 | 43 | return result; 44 | } 45 | 46 | // ------------------------------------------------------------------------- 47 | 48 | private void processEmail( Messaging.InboundEmail email ) { 49 | 50 | System.debug( 'processing email' ); 51 | 52 | List requests = (List) JSON.deserialize( email.plainTextBody, List.class ); 53 | 54 | new ChatterBotPostMessageService().processRequests( requests ); 55 | 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageEmailHandler.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageEmailHandlerTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Developed by Doug Ayers (douglascayers.com) 3 | * https://github.com/DouglasCAyers/salesforce-chatter-bot-feeds 4 | * 5 | * Unfortunately, when testing ConnectApi methods Salesforce requires SeeAllData = true. 6 | * If you don't then you'll get an error: "System.UnsupportedOperationException: ConnectApi methods are not supported in data siloed tests. Please use @IsTest(SeeAllData=true)." 7 | */ 8 | @isTest( seeAllData = true ) 9 | private class ChatterBotPostMessageEmailHandlerTest { 10 | 11 | private static ChatterBotPostMessageInvocable.Request buildRequest( String authorId, String subjectId, String recordId, String emailTemplateName ) { 12 | 13 | ChatterBotPostMessageInvocable.Request request = new ChatterBotPostMessageInvocable.Request(); 14 | 15 | request.authorId = authorId; 16 | request.subjectId = subjectId; 17 | request.recordId = recordId; 18 | request.emailTemplateName = emailTemplateName; 19 | 20 | return request; 21 | } 22 | 23 | @isTest( seeAllData = true ) 24 | static void test_post_message_fail() { 25 | 26 | CollaborationGroup grp = new CollaborationGroup( 27 | name = 'Test Group ' + DateTime.now().getTime(), 28 | collaborationType = 'Public' 29 | ); 30 | 31 | insert grp; 32 | 33 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 34 | req.authorId = UserInfo.getUserId(); 35 | req.subjectId = grp.id; 36 | req.recordId = UserInfo.getUserId(); 37 | req.emailTemplateName = 'Chatter_Bot_Post_Message_Template'; 38 | 39 | Messaging.InboundEmail email = new Messaging.InboundEmail(); 40 | email.subject = 'Chatter Bot Post Message'; 41 | email.plainTextBody = null; 42 | 43 | Messaging.InboundEnvelope env = new Messaging.InboundEnvelope(); 44 | 45 | Test.startTest(); 46 | 47 | ChatterBotPostMessageEmailHandler handler = new ChatterBotPostMessageEmailHandler(); 48 | Messaging.InboundEmailResult result = handler.handleInboundEmail( email, env ); 49 | 50 | Test.stopTest(); 51 | 52 | System.assertEquals( false, result.success ); 53 | 54 | } 55 | 56 | @isTest( seeAllData = true ) 57 | static void test_post_message() { 58 | 59 | CollaborationGroup grp = new CollaborationGroup( 60 | name = 'Test Group ' + DateTime.now().getTime(), 61 | collaborationType = 'Public' 62 | ); 63 | 64 | insert grp; 65 | 66 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 67 | req.authorId = UserInfo.getUserId(); 68 | req.subjectId = grp.id; 69 | req.recordId = UserInfo.getUserId(); 70 | req.emailTemplateName = 'Chatter_Bot_Post_Message_Template'; 71 | 72 | Messaging.InboundEmail email = new Messaging.InboundEmail(); 73 | email.subject = 'Chatter Bot Post Message'; 74 | email.plainTextBody = JSON.serialize( new List{ req } ); 75 | 76 | Messaging.InboundEnvelope env = new Messaging.InboundEnvelope(); 77 | 78 | Test.startTest(); 79 | 80 | ChatterBotPostMessageEmailHandler handler = new ChatterBotPostMessageEmailHandler(); 81 | Messaging.InboundEmailResult result = handler.handleInboundEmail( email, env ); 82 | 83 | Test.stopTest(); 84 | 85 | System.assertEquals( true, result.success ); 86 | 87 | FeedItem fi = [ SELECT id, parentId, body FROM FeedItem WHERE parentId = :grp.id LIMIT 1 ]; 88 | 89 | System.debug( fi ); 90 | 91 | } 92 | 93 | @isTest( seeAllData = true ) 94 | static void test_post_messages() { 95 | 96 | CollaborationGroup grp = new CollaborationGroup( 97 | name = 'Test Group ' + DateTime.now().getTime(), 98 | collaborationType = 'Public' 99 | ); 100 | 101 | insert grp; 102 | 103 | List requests = new List(); 104 | 105 | for ( Integer i = 0; i < 50; i++ ) { 106 | requests.add( buildRequest( 107 | UserInfo.getUserId(), // author 108 | grp.id, // subject 109 | UserInfo.getUserId(), // template merge record 110 | 'Chatter_Bot_Post_Message_Template' 111 | )); 112 | } 113 | 114 | Messaging.InboundEmail email = new Messaging.InboundEmail(); 115 | email.subject = 'Chatter Bot Post Message'; 116 | email.plainTextBody = JSON.serialize( requests ); 117 | 118 | Messaging.InboundEnvelope env = new Messaging.InboundEnvelope(); 119 | 120 | Test.startTest(); 121 | 122 | ChatterBotPostMessageEmailHandler handler = new ChatterBotPostMessageEmailHandler(); 123 | Messaging.InboundEmailResult result = handler.handleInboundEmail( email, env ); 124 | 125 | Test.stopTest(); 126 | 127 | System.assertEquals( true, result.success ); 128 | 129 | List feedItems = new List([ SELECT id, parentId, body FROM FeedItem WHERE parentId = :grp.id ]); 130 | 131 | System.debug( feedItems ); 132 | 133 | System.assertEquals( requests.size(), feedItems.size() ); 134 | 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageEmailHandlerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageInvocable.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Developed by Doug Ayers (douglascayers.com) 3 | * https://github.com/DouglasCAyers/salesforce-chatter-bot-feeds 4 | */ 5 | public without sharing class ChatterBotPostMessageInvocable { 6 | 7 | @InvocableMethod( 8 | label = 'CB: Post Message' 9 | description = 'Posts a Chatter message as specified user.' 10 | ) 11 | public static void execute( List requests ) { 12 | 13 | System.debug( 'ChatterBotPostMessageInvocable.execute: ' + requests ); 14 | 15 | Chatter_Bot_Feeds_Setting__c settings = Chatter_Bot_Feeds_Setting__c.getInstance(); 16 | 17 | if ( String.isBlank( settings.email_service_address_user_id__c ) ) { 18 | throw new ChatterBotPostMessageException( 'Missing Chatter_Bot_Feeds_Setting__c.Email_Service_Address_User_ID__c. Please update custom setting with email service address for ChatterBotPostMessageEmailHandler.' ); 19 | } 20 | 21 | Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage(); 22 | message.setTargetObjectId( settings.email_service_address_user_id__c ); 23 | message.setTreatTargetObjectAsRecipient( true ); 24 | message.setSaveAsActivity( false ); 25 | message.setPlainTextBody( JSON.serialize( requests ) ); 26 | 27 | List messages = new List(); 28 | messages.add( message ); 29 | 30 | Boolean allOrNone = false; 31 | 32 | List results = Messaging.sendEmail( messages, allOrNone ); 33 | 34 | for ( Messaging.SendEmailResult result : results ) { 35 | if ( !result.isSuccess() ) { 36 | for ( Messaging.SendEmailError err : result.getErrors() ) { 37 | System.debug( LoggingLevel.ERROR, err ); 38 | } 39 | } 40 | } 41 | 42 | } 43 | 44 | // ----------------------------------------------------------------- 45 | 46 | public class Request { 47 | 48 | @InvocableVariable( 49 | label = 'Author User ID' 50 | description = 'Who the Chatter post will be shown as created by.' 51 | required = true 52 | ) 53 | public String authorId; 54 | 55 | @InvocableVariable( 56 | label = 'User, Group, or Record ID' 57 | description = 'Where the Chatter post will be made.' 58 | required = true 59 | ) 60 | public String subjectId; 61 | 62 | @InvocableVariable( 63 | label = 'Chatter Message' 64 | description = 'The message to post. One of "Chatter Message" or "Email Template Unique Name" must be specified.' 65 | ) 66 | public String message; 67 | 68 | @InvocableVariable( 69 | label = 'Email Template Unique Name' 70 | description = 'An email template to use for generating the rich-text Chatter post message. One of "Chatter Message" or "Email Template Unique Name" must be specified.' 71 | ) 72 | public String emailTemplateName; 73 | 74 | @InvocableVariable( 75 | label = 'Record ID (Template Merge Fields)' 76 | description = 'Identifies a record such as an Account or Contact that will be read and used in merge field processing of the email template.' 77 | ) 78 | public String recordId; 79 | 80 | } 81 | 82 | // ----------------------------------------------------------------- 83 | 84 | public class ChatterBotPostMessageException extends Exception {} 85 | 86 | } -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageInvocable.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageInvocableTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Developed by Doug Ayers (douglascayers.com) 3 | * https://github.com/DouglasCAyers/salesforce-chatter-bot-feeds 4 | */ 5 | @isTest 6 | private class ChatterBotPostMessageInvocableTest { 7 | 8 | @isTest 9 | static void test_success() { 10 | 11 | Chatter_Bot_Feeds_Setting__c settings = Chatter_Bot_Feeds_Setting__c.getInstance(); 12 | settings.email_service_address_user_id__c = UserInfo.getUserId(); 13 | upsert settings; 14 | 15 | Test.startTest(); 16 | 17 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 18 | req.authorId = UserInfo.getUserId(); 19 | req.subjectId = UserInfo.getUserId(); 20 | req.message = 'Hello World'; 21 | 22 | ChatterBotPostMessageInvocable.execute( new List{ req } ); 23 | 24 | Test.stopTest(); 25 | 26 | } 27 | 28 | @isTest 29 | static void test_failure() { 30 | 31 | try { 32 | 33 | Test.startTest(); 34 | 35 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 36 | req.authorId = UserInfo.getUserId(); 37 | req.subjectId = UserInfo.getUserId(); 38 | req.message = 'Hello World'; 39 | 40 | ChatterBotPostMessageInvocable.execute( new List{ req } ); 41 | 42 | Test.stopTest(); 43 | 44 | System.assert( false, 'Should fail' ); 45 | 46 | } catch ( ChatterBotPostMessageInvocable.ChatterBotPostMessageException e ) { 47 | 48 | System.assert( e.getMessage().contains( 'Missing Chatter_Bot_Feeds_Setting__c.Email_Service_Address_User_ID__c' ) ); 49 | 50 | } 51 | 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageInvocableTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageService.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Developed by Doug Ayers (douglascayers.com) 3 | * https://github.com/DouglasCAyers/salesforce-chatter-bot-feeds 4 | */ 5 | public without sharing class ChatterBotPostMessageService { 6 | 7 | private static final String FEED_ITEM_KEY_PREFIX = FeedItem.sObjectType.getDescribe().getKeyPrefix(); 8 | 9 | public void processRequests( List requests ) { 10 | 11 | System.debug( 'processing requests' ); 12 | 13 | Set emailTemplateNames = new Set(); 14 | Set subjectIds = new Set(); 15 | 16 | for ( ChatterBotPostMessageInvocable.Request request : requests ) { 17 | 18 | if ( String.isNotBlank( request.emailTemplateName ) ) { 19 | emailTemplateNames.add( request.emailTemplateName ); 20 | } 21 | 22 | if ( String.isNotBlank( request.subjectId ) ) { 23 | subjectIds.add( request.subjectId ); 24 | } 25 | 26 | } 27 | 28 | Map emailTemplateNameToIdMap = queryEmailTemplateNameToIdMap( emailTemplateNames ); 29 | Map subjectIdToNetworkIdMap = querySubjectIdToNetworkIdMap( subjectIds ); 30 | 31 | for ( ChatterBotPostMessageInvocable.Request request : requests ) { 32 | processRequest( request, emailTemplateNameToIdMap, subjectIdToNetworkIdMap ); 33 | } 34 | 35 | } 36 | 37 | // ----------------------------------------------------- 38 | 39 | private void processRequest( ChatterBotPostMessageInvocable.Request request, Map emailTemplateNameToIdMap, Map subjectIdToNetworkIdMap ) { 40 | 41 | System.debug( 'processing request: ' + request ); 42 | 43 | String authorId = request.authorId; // who is posting the message 44 | String subjectId = request.subjectId; // where the post is being made: user, group, record 45 | String recordId = request.recordId; // if using email template, the record used to satisfy merge fields 46 | String message = request.message; // if not using email template, the chatter message to post 47 | String emailTemplateName = request.emailTemplateName; 48 | String networkId = Network.getNetworkId(); 49 | 50 | if ( String.isBlank( authorId ) ) { 51 | throw new ChatterBotPostMessageException( 'Missing "authorId" to indicate which user is authoring this Chatter post.' ); 52 | } 53 | 54 | if ( String.isBlank( subjectId ) ) { 55 | throw new ChatterBotPostMessageException( 'Missing "subjectId" to indicate where to post this message: User, Group, or Record ID.' ); 56 | } else { 57 | networkId = subjectIdToNetworkIdMap.get( subjectId ); 58 | } 59 | 60 | if ( String.isNotBlank( emailTemplateName ) ) { 61 | 62 | ID emailTemplateId = emailTemplateNameToIdMap.get( emailTemplateName ); 63 | 64 | if ( String.isBlank( emailTemplateId ) ) { 65 | throw new ChatterBotPostMessageException( 'No email template found by unique name: ' + emailTemplateName ); 66 | } 67 | 68 | Messaging.SingleEmailMessage emailMessage = Messaging.renderStoredEmailTemplate( emailTemplateId, authorId, recordId ); 69 | 70 | message = emailMessage.getHtmlBody(); 71 | 72 | } 73 | 74 | if ( String.isBlank( message ) ) { 75 | throw new ChatterBotPostMessageException( 'One of either "message" or "emailTemplateName" must be provided as the content of the Chatter post.' ); 76 | } 77 | 78 | postChatterMessage( networkId, authorId, subjectId, message ); 79 | 80 | } 81 | 82 | /** 83 | * Determines if we should post a FeedItem or FeedComment based on the subject id. 84 | * If the subject is itself a Chatter FeedItem then we will post a comment to that thread. 85 | * Otherwise we post a new Chatter message. 86 | */ 87 | private void postChatterMessage( ID networkId, ID authorId, ID subjectId, String message ) { 88 | 89 | System.debug( 'posting chatter message: networkId=' + networkId + ', authorId=' + authorId + ', subjectId=' + subjectId + ', message=' + message ); 90 | 91 | // if message uses Flow or Process Builder @mention syntax @[user_or_group_id] 92 | // unfortunately the ConnectApi doesn't support that. It won't error, but won't 93 | // resolve to an expected @mention. So we need to translate that syntax into 94 | // the syntax expected by ConnectApi, which is simply {user_or_group_id}. 95 | // Now, you might say, well, tell folks to use that new syntax instead. 96 | // Unfortunately, if you try to use that {id} syntax in a Flow Text Template 97 | // then it misinterprets and tries to pre-emptively resolve the variable and 98 | // results in {Notfound} rendered. Not what we want.... 99 | Matcher mentionMatcher = Pattern.compile( '(@\\[([a-zA-Z0-9]+)\\])' ).matcher( message ); 100 | while ( mentionMatcher.find() ) { 101 | message = mentionMatcher.replaceAll( '{$2}' ); 102 | } 103 | 104 | String subjectKeyPrefix = String.valueOf( subjectId ).left( 3 ); 105 | 106 | if ( subjectKeyPrefix == FEED_ITEM_KEY_PREFIX ) { 107 | postFeedComment( networkId, authorId, subjectId, message ); 108 | } else { 109 | postFeedItem( networkId, authorId, subjectId, message ); 110 | } 111 | 112 | } 113 | 114 | /** 115 | * Posts a new message related to given subject. 116 | * SubjectId must not be another FeedItem or FeedComment but rather 117 | * a User ID, Chatter Group ID, or other record ID like an Account, Contact, etc. 118 | */ 119 | private void postFeedItem( ID networkId, ID authorId, ID subjectId, String message ) { 120 | 121 | System.debug( 'posting feed item: networkId=' + networkId + ', authorId=' + authorId + ', subjectId=' + subjectId + ', message=' + message ); 122 | 123 | // we use a mix of FeedItem DML and ConnectApi for our requirements: 124 | // 1. Post Chatter Message as any user (FeedItem DML) 125 | // 2. Post Rich-Text content with @mentions (ConnectApi) 126 | 127 | // setting the createdById only works if the context user of this email service 128 | // has the system permission "Insert System Field Values for Chatter Feeds" 129 | 130 | // even though rich-text is supported with FeedItem DML, 131 | // all @mention links are converted to plain text. 132 | // workaround is that after inserting the FeedItem we use ConnectApi 133 | // to update the element with the same rich-text but @mentions will be preserved. 134 | 135 | FeedItem fi = new FeedItem( 136 | parentId = subjectId, // where post is being made: user, group, record 137 | createdById = authorId, // who is posting the message 138 | body = message, // rich-text, but @mentions not supported 139 | isRichText = true // we support rich-text and @mentions 140 | ); 141 | 142 | insert fi; 143 | 144 | System.debug( fi ); 145 | 146 | // after creating the shell of the feed item, retrieve the record back in Chatter for Apex 147 | ConnectApi.FeedElement fe = ConnectApi.ChatterFeeds.getFeedElement( networkId, fi.id ); 148 | 149 | // parse the rich-text message and create new message input 150 | ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput(); 151 | messageInput.messageSegments = ConnectApiHelper.getMessageSegmentInputs( message ); 152 | 153 | // define an updated feed element using the rich-text message 154 | ConnectApi.FeedItemInput input = new ConnectApi.FeedItemInput(); 155 | input.body = messageInput; 156 | 157 | // replace the content of the chatter post 158 | fe = ConnectApi.ChatterFeeds.updateFeedElement( networkId, fe.id, input ); 159 | 160 | System.debug( fe ); 161 | 162 | } 163 | 164 | /** 165 | * Posts a comment to an existing feed item. 166 | * SubjectId must be ID of a FeedItem record. 167 | */ 168 | private void postFeedComment( ID networkId, ID authorId, ID subjectId, String message ) { 169 | 170 | System.debug( 'posting feed comment: networkId=' + networkId + ', authorId=' + authorId + ', subjectId=' + subjectId + ', message=' + message ); 171 | 172 | // we use a mix of FeedComment DML and ConnectApi for our requirements: 173 | // 1. Post Chatter Message as any user (FeedComment DML) 174 | // 2. Post Rich-Text content with @mentions (ConnectApi) 175 | 176 | // setting the createdById only works if the context user of this email service 177 | // has the system permission "Insert System Field Values for Chatter Feeds" 178 | 179 | // even though rich-text is supported with FeedComment DML, 180 | // all @mention links are converted to plain text. 181 | // workaround is that after inserting the FeedComment we use ConnectApi 182 | // to update the element with the same rich-text but @mentions will be preserved. 183 | 184 | FeedComment fc = new FeedComment( 185 | feedItemId = subjectId, // where post is being made: user, group, record 186 | createdById = authorId, // who is posting the message 187 | commentBody = message, // rich-text, but @mentions not supported 188 | isRichText = true // we support rich-text and @mentions 189 | ); 190 | 191 | insert fc; 192 | 193 | System.debug( fc ); 194 | 195 | // after creating the shell of the feed comment, retrieve the record back in Chatter for Apex 196 | ConnectApi.Comment comment = ConnectApi.ChatterFeeds.getComment( networkId, fc.id ); 197 | 198 | // parse the rich-text message and create new message input 199 | ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput(); 200 | messageInput.messageSegments = ConnectApiHelper.getMessageSegmentInputs( message ); 201 | 202 | // define an updated feed element using the rich-text message 203 | ConnectApi.CommentInput input = new ConnectApi.CommentInput(); 204 | input.body = messageInput; 205 | 206 | // replace the content of the chatter post 207 | comment = ConnectApi.ChatterFeeds.updateComment( networkId, comment.id, input ); 208 | 209 | System.debug( comment ); 210 | 211 | } 212 | 213 | private Map queryEmailTemplateNameToIdMap( Set emailTemplateNames ) { 214 | 215 | System.debug( 'building email template map' ); 216 | System.debug( 'emailTemplateNames=' + emailTemplateNames ); 217 | 218 | Map emailTemplateNameToIdMap = new Map(); 219 | 220 | if ( emailTemplateNames.size() > 0 ) { 221 | 222 | List templates = new List([ 223 | SELECT 224 | id, developerName 225 | FROM 226 | EmailTemplate 227 | WHERE 228 | developerName IN :emailTemplateNames 229 | ]); 230 | 231 | for ( EmailTemplate template : templates ) { 232 | emailTemplateNameToIdMap.put( template.developerName, template.id ); 233 | } 234 | 235 | } 236 | 237 | return emailTemplateNameToIdMap; 238 | } 239 | 240 | private Map querySubjectIdToNetworkIdMap( Set subjectIds ) { 241 | 242 | System.debug( 'building subject => network map' ); 243 | System.debug( 'subjectIds=' + subjectIds ); 244 | 245 | Map subjectIdToNetworkIdMap = new Map(); 246 | 247 | DescribeSObjectResult groupDescribe = CollaborationGroup.sObjectType.getDescribe(); 248 | Map groupFieldsMap = groupDescribe.fields.getMap(); 249 | Boolean groupHasNetworkId = ( groupFieldsMap.containsKey( 'NetworkId' ) ); 250 | 251 | System.debug( 'groupHasNetworkId: ' + groupHasNetworkId ); 252 | 253 | // if posting to a group and the group belongs to a specific community then 254 | // we will use that as the network id. Note, the NetworkId field only exists 255 | // if an org has a community created. 256 | if ( groupHasNetworkId ) { 257 | 258 | // since an org may or may not have NetworkId field available 259 | // depending on if they have a community or not then we need 260 | // to use dynamic query to avoid compilation issues 261 | for ( CollaborationGroup grp : Database.query( 'SELECT id, networkId FROM CollaborationGroup WHERE id IN :subjectIds' ) ) { 262 | 263 | Object fieldValue = grp.get( 'networkId' ); 264 | 265 | if ( fieldValue != null ) { 266 | subjectIdToNetworkIdMap.put( grp.id, String.valueOf( fieldValue ) ); 267 | } 268 | 269 | } 270 | 271 | } 272 | 273 | return subjectIdToNetworkIdMap; 274 | } 275 | 276 | public class ChatterBotPostMessageException extends Exception {} 277 | 278 | } -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageService.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageServiceTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Developed by Doug Ayers (douglascayers.com) 3 | * https://github.com/DouglasCAyers/salesforce-chatter-bot-feeds 4 | * 5 | * Unfortunately, when testing ConnectApi methods Salesforce requires SeeAllData = true. 6 | * If you don't then you'll get an error: "System.UnsupportedOperationException: ConnectApi methods are not supported in data siloed tests. Please use @IsTest(SeeAllData=true)." 7 | */ 8 | @isTest( seeAllData = true ) 9 | private class ChatterBotPostMessageServiceTest { 10 | 11 | private static ChatterBotPostMessageInvocable.Request buildRequest( String authorId, String subjectId, String recordId, String emailTemplateName ) { 12 | 13 | ChatterBotPostMessageInvocable.Request request = new ChatterBotPostMessageInvocable.Request(); 14 | 15 | request.authorId = authorId; 16 | request.subjectId = subjectId; 17 | request.recordId = recordId; 18 | request.emailTemplateName = emailTemplateName; 19 | 20 | return request; 21 | } 22 | 23 | @isTest( seeAllData = true ) 24 | static void test_post_message() { 25 | 26 | CollaborationGroup grp = new CollaborationGroup( 27 | name = 'Test Group ' + DateTime.now().getTime(), 28 | collaborationType = 'Public' 29 | ); 30 | 31 | insert grp; 32 | 33 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 34 | req.authorId = UserInfo.getUserId(); 35 | req.subjectId = grp.id; 36 | req.recordId = UserInfo.getUserId(); 37 | req.emailTemplateName = 'Chatter_Bot_Post_Message_Template'; 38 | 39 | Test.startTest(); 40 | 41 | new ChatterBotPostMessageService().processRequests( new List{ req } ); 42 | 43 | Test.stopTest(); 44 | 45 | FeedItem fi = [ SELECT id, parentId, body FROM FeedItem WHERE parentId = :grp.id LIMIT 1 ]; 46 | 47 | System.debug( fi ); 48 | 49 | } 50 | 51 | @isTest( seeAllData = true ) 52 | static void test_post_messages() { 53 | 54 | CollaborationGroup grp = new CollaborationGroup( 55 | name = 'Test Group ' + DateTime.now().getTime(), 56 | collaborationType = 'Public' 57 | ); 58 | 59 | insert grp; 60 | 61 | List requests = new List(); 62 | 63 | for ( Integer i = 0; i < 50; i++ ) { 64 | requests.add( buildRequest( 65 | UserInfo.getUserId(), // author 66 | grp.id, // subject 67 | UserInfo.getUserId(), // template merge record 68 | 'Chatter_Bot_Post_Message_Template' 69 | )); 70 | } 71 | 72 | Test.startTest(); 73 | 74 | new ChatterBotPostMessageService().processRequests( requests ); 75 | 76 | Test.stopTest(); 77 | 78 | List feedItems = new List([ SELECT id, parentId, body FROM FeedItem WHERE parentId = :grp.id ]); 79 | 80 | System.debug( feedItems ); 81 | 82 | System.assertEquals( requests.size(), feedItems.size() ); 83 | 84 | } 85 | 86 | @IsTest( seeAllData = true ) 87 | static void test_post_comment() { 88 | 89 | FeedItem fi = new FeedItem( 90 | parentId = UserInfo.getUserId(), 91 | body = 'Chatter Post' 92 | ); 93 | 94 | insert fi; 95 | 96 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 97 | req.authorId = UserInfo.getUserId(); 98 | req.subjectId = fi.id; 99 | req.recordId = UserInfo.getUserId(); 100 | req.emailTemplateName = 'Chatter_Bot_Post_Message_Template'; 101 | 102 | Test.startTest(); 103 | 104 | new ChatterBotPostMessageService().processRequests( new List{ req } ); 105 | 106 | Test.stopTest(); 107 | 108 | FeedComment comment = [ SELECT id FROM FeedComment WHERE feedItemId = :fi.id LIMIT 1 ]; 109 | 110 | System.debug( comment ); 111 | 112 | } 113 | 114 | @isTest( seeAllData = true ) 115 | static void test_missing_authorId() { 116 | 117 | CollaborationGroup grp = new CollaborationGroup( 118 | name = 'Test Group ' + DateTime.now().getTime(), 119 | collaborationType = 'Public' 120 | ); 121 | 122 | insert grp; 123 | 124 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 125 | req.authorId = null; 126 | req.subjectId = grp.id; 127 | req.recordId = UserInfo.getUserId(); 128 | req.emailTemplateName = 'Chatter_Bot_Post_Message_Template'; 129 | 130 | try { 131 | 132 | Test.startTest(); 133 | 134 | new ChatterBotPostMessageService().processRequests( new List{ req } ); 135 | 136 | Test.stopTest(); 137 | 138 | } catch ( ChatterBotPostMessageService.ChatterBotPostMessageException e ) { 139 | 140 | System.assert( e.getMessage().contains( 'Missing "authorId"' ) ); 141 | 142 | } 143 | 144 | } 145 | 146 | @isTest( seeAllData = true ) 147 | static void test_missing_subjectId() { 148 | 149 | CollaborationGroup grp = new CollaborationGroup( 150 | name = 'Test Group ' + DateTime.now().getTime(), 151 | collaborationType = 'Public' 152 | ); 153 | 154 | insert grp; 155 | 156 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 157 | req.authorId = UserInfo.getUserId(); 158 | req.subjectId = null; 159 | req.recordId = UserInfo.getUserId(); 160 | req.emailTemplateName = 'Chatter_Bot_Post_Message_Template'; 161 | 162 | try { 163 | 164 | Test.startTest(); 165 | 166 | new ChatterBotPostMessageService().processRequests( new List{ req } ); 167 | 168 | Test.stopTest(); 169 | 170 | } catch ( ChatterBotPostMessageService.ChatterBotPostMessageException e ) { 171 | 172 | System.assert( e.getMessage().contains( 'Missing "subjectId"' ) ); 173 | 174 | } 175 | 176 | } 177 | 178 | @isTest( seeAllData = true ) 179 | static void test_missing_template() { 180 | 181 | CollaborationGroup grp = new CollaborationGroup( 182 | name = 'Test Group ' + DateTime.now().getTime(), 183 | collaborationType = 'Public' 184 | ); 185 | 186 | insert grp; 187 | 188 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 189 | req.authorId = UserInfo.getUserId(); 190 | req.subjectId = grp.id; 191 | req.recordId = UserInfo.getUserId(); 192 | req.emailTemplateName = String.valueOf( DateTime.now().getTime() ); // doesn't exist 193 | 194 | try { 195 | 196 | Test.startTest(); 197 | 198 | new ChatterBotPostMessageService().processRequests( new List{ req } ); 199 | 200 | Test.stopTest(); 201 | 202 | } catch ( ChatterBotPostMessageService.ChatterBotPostMessageException e ) { 203 | 204 | System.assert( e.getMessage().contains( 'No email template found' ) ); 205 | 206 | } 207 | 208 | } 209 | 210 | @isTest( seeAllData = true ) 211 | static void test_missing_message() { 212 | 213 | CollaborationGroup grp = new CollaborationGroup( 214 | name = 'Test Group ' + DateTime.now().getTime(), 215 | collaborationType = 'Public' 216 | ); 217 | 218 | insert grp; 219 | 220 | ChatterBotPostMessageInvocable.Request req = new ChatterBotPostMessageInvocable.Request(); 221 | req.authorId = UserInfo.getUserId(); 222 | req.subjectId = grp.id; 223 | req.recordId = UserInfo.getUserId(); 224 | req.emailTemplateName = null; 225 | 226 | try { 227 | 228 | Test.startTest(); 229 | 230 | new ChatterBotPostMessageService().processRequests( new List{ req } ); 231 | 232 | Test.stopTest(); 233 | 234 | } catch ( ChatterBotPostMessageService.ChatterBotPostMessageException e ) { 235 | 236 | System.assert( e.getMessage().contains( 'One of either "message" or "emailTemplateName"' ) ); 237 | 238 | } 239 | 240 | } 241 | 242 | } -------------------------------------------------------------------------------- /src/classes/ChatterBotPostMessageServiceTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ConnectApiHelper.cls: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2016, salesforce.com, Inc. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the salesforce.com, Inc. nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 25 | OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | /** 29 | * 30 | * Helper class that makes it easier to do common operations with the classes in the ConnectApi namespace. 31 | * 32 | * Includes convenience methods to: 33 | * 34 | * - Post Chatter @-mentions, rich text, and inline images with Apex code. 35 | * - Take a feed item or comment body and return an input body that matches it. 36 | * This is useful for when you retrieve a feed item or comment and want to either 37 | * re-post or edit it. 38 | * 39 | * This class works with API version 36.0 and later. There are separate classes 40 | * that work with v35.0 and earlier. 41 | * 42 | * See https://github.com/forcedotcom/ConnectApiHelper for more information. 43 | * 44 | */ 45 | 46 | global class ConnectApiHelper { 47 | 48 | public class InvalidParameterException extends Exception {} 49 | 50 | private static final Map supportedMarkup = new Map { 51 | 'b' => ConnectApi.MarkupType.Bold, 52 | 'i' => ConnectApi.MarkupType.Italic, 53 | 'li' => ConnectApi.MarkupType.ListItem, 54 | 'ol' => ConnectApi.MarkupType.OrderedList, 55 | 'p' => ConnectApi.MarkupType.Paragraph, 56 | 's' => ConnectApi.MarkupType.Strikethrough, 57 | 'u' => ConnectApi.MarkupType.Underline, 58 | 'ul' => ConnectApi.MarkupType.UnorderedList 59 | }; 60 | 61 | /** 62 | * Posts a feed item with @-mentions using an @-mention formatting syntax. 63 | * 64 | * @param communityId Use either the ID of a community, 'internal', or null. 65 | * @param subjectId The parent of the post. Can be a user ID, a group ID, or a record ID. 66 | * @param textWithMentions The text of the post. You can @-mention a user or group by using 67 | * the syntax {ID}, for example: 'Hello {005x0000000URNP}, have you 68 | * seen the group {0F9x00000000D7m}?' Links and hashtags will be 69 | * automatically parsed if provided. 70 | * @return The posted feed item. 71 | */ 72 | public static ConnectApi.FeedElement postFeedItemWithMentions(String communityId, String subjectId, String textWithMentions) { 73 | 74 | return postFeedItemWithSpecialFormatting(communityId, subjectId, textWithMentions, 'textWithMentions'); 75 | } 76 | 77 | /** 78 | * Posts a feed item with rich text using HTML tags and inline image formatting syntax. 79 | * 80 | * @param communityId Use either the ID of a community, 'internal', or null. 81 | * @param subjectId The parent of the post. Can be a user ID, a group ID, or a record ID. 82 | * @param textWithMentionsAndRichText The text of the post. You can @-mention a 83 | * user or group by using the syntax {ID}, for example: 84 | * 'Hello {005x0000000URNP}, have you seen the group {0F9x00000000D7m}?' 85 | * You can include rich text by using supported HTML tags: 86 | * , , , ,
    ,
      ,
    1. ,

      . 87 | * You can include an inline image by using the syntax {img:ID} or 88 | * {img:ID:alt text}, for example: 'Have you seen this gorgeous view? 89 | * {img:069x00000000D7m:View of the Space Needle from our office.}?' 90 | * Links and hashtags will be automatically parsed if provided. 91 | * @return The posted feed item. 92 | */ 93 | public static ConnectApi.FeedElement postFeedItemWithRichText(String communityId, String subjectId, String textWithMentionsAndRichText) { 94 | return postFeedItemWithSpecialFormatting(communityId, subjectId, textWithMentionsAndRichText, 'textWithMentionsAndRichText'); 95 | } 96 | 97 | private static ConnectApi.FeedElement postFeedItemWithSpecialFormatting(String communityId, String subjectId, String formattedText, String textParameterName) { 98 | if (formattedText == null || formattedText.trim().length() == 0) { 99 | throw new InvalidParameterException('The ' + textParameterName + ' parameter must be non-empty.'); 100 | } 101 | 102 | ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput(); 103 | messageInput.messageSegments = getMessageSegmentInputs(formattedText); 104 | 105 | ConnectApi.FeedItemInput input = new ConnectApi.FeedItemInput(); 106 | input.body = messageInput; 107 | input.subjectId = subjectId; 108 | 109 | return ConnectApi.ChatterFeeds.postFeedElement(communityId, input); 110 | } 111 | 112 | /** 113 | * Posts a comment with @-mentions using an @-mention formatting syntax. 114 | * 115 | * @param communityId Use either the ID of a community, 'internal', or null. 116 | * @param feedItemId The ID of the feed item being commented on. 117 | * @param textWithMentions The text of the comment. You can @-mention a user or group by using 118 | * the syntax {ID}, for example: 'Hello {005x0000000URNP}, have you 119 | * seen the group {0F9x00000000D7m}?' Links and hashtags will be 120 | * automatically parsed if provided. 121 | * @return The posted comment. 122 | */ 123 | public static ConnectApi.Comment postCommentWithMentions(String communityId, String feedItemId, String textWithMentions) { 124 | 125 | if (textWithMentions == null || textWithMentions.trim().length() == 0) { 126 | throw new InvalidParameterException('The textWithMentions parameter must be non-empty.'); 127 | } 128 | 129 | ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput(); 130 | messageInput.messageSegments = getMessageSegmentInputs(textWithMentions); 131 | 132 | ConnectApi.CommentInput input = new ConnectApi.CommentInput(); 133 | input.body = messageInput; 134 | 135 | return ConnectApi.ChatterFeeds.postCommentToFeedElement(communityId, feedItemId, input, null); 136 | } 137 | 138 | public static List getMessageSegmentInputs(String inputText) { 139 | if (inputText == null) { 140 | throw new InvalidParameterException('The inputText parameter cannot be null.'); 141 | } 142 | 143 | List messageSegmentInputs = new List(); 144 | Integer strPos = 0; 145 | // The pattern for matching mentions, markup begin/end tags, and inline images. 146 | // The first group matches a 15 or 18 character ID surrounded by {}: 147 | // (\\{[a-zA-Z0-9]{15}\\}|\\{[a-zA-Z0-9]{18}\\}) 148 | // The second/third groups match beginning/ending HTML tags: (<[a-zA-Z]*>)|() 149 | // The fourth group matches a 15 or 18 character content document ID preceded by "img:", 150 | // optionally followed by a string (not containing '}'), and surrounded by {}: 151 | // (\\{img:(069[a-zA-Z0-9]{12,15})(:[\\s\\S]*?)?\\}) 152 | Pattern globalPattern = Pattern.compile('(\\{[a-zA-Z0-9]{15}\\}|\\{[a-zA-Z0-9]{18}\\})|(<[a-zA-Z]*>)|()|(\\{img:(069[a-zA-Z0-9]{12,15})(:[\\s\\S]*?)?\\})'); 153 | Matcher globalMatcher = globalPattern.matcher(inputText); 154 | 155 | while (globalMatcher.find()) { 156 | String textSegment = inputText.substring(strPos, globalMatcher.start()); 157 | String matchingText = globalMatcher.group(); 158 | if (matchingText.startsWith('{')) { 159 | // Add a segment for any accumulated text (which includes unsupported HTML tags). 160 | addTextSegment(messageSegmentInputs, textSegment); 161 | 162 | // Strip off the { and }. 163 | String innerMatchedText = matchingText.substring(1, matchingText.length() - 1); 164 | 165 | if (innerMatchedText.startsWith('img:')) { 166 | // This is an inline image. 167 | String[] imageInfo = innerMatchedText.split(':', 3); 168 | String altText = imageInfo.size() == 3 ? imageInfo[2] : null; 169 | ConnectApi.InlineImageSegmentInput inlineImageSegmentInput = makeInlineImageSegmentInput(imageInfo[1], altText); 170 | messageSegmentInputs.add(inlineImageSegmentInput); 171 | strPos = globalMatcher.end(); 172 | } 173 | else { 174 | // This is a mention id. 175 | ConnectApi.MentionSegmentInput mentionSegmentInput = makeMentionSegmentInput(innerMatchedText); 176 | messageSegmentInputs.add(mentionSegmentInput); 177 | strPos = globalMatcher.end(); 178 | } 179 | } 180 | else { 181 | // This is an HTML tag. 182 | boolean isBeginTag = !matchingText.startsWith('. 185 | String tag = matchingText.substring(1, matchingText.indexOf('>')); 186 | if (supportedMarkup.containsKey(tag.toLowerCase())) { 187 | // Add a segment for any accumulated text (which includes unsupported HTML tags). 188 | addTextSegment(messageSegmentInputs, textSegment); 189 | 190 | ConnectApi.MarkupBeginSegmentInput markupBeginSegmentInput = makeMarkupBeginSegmentInput(tag); 191 | messageSegmentInputs.add(markupBeginSegmentInput); 192 | strPos = globalMatcher.end(); 193 | } 194 | } 195 | else { // This is an end tag. 196 | // Strip off the . 197 | String tag = matchingText.substring(2, matchingText.indexOf('>')); 198 | if (supportedMarkup.containsKey(tag.toLowerCase())) { 199 | // Add a segment for any accumulated text (which includes unsupported HTML tags). 200 | addTextSegment(messageSegmentInputs, textSegment); 201 | 202 | ConnectApi.MarkupEndSegmentInput markupEndSegmentInput = makeMarkupEndSegmentInput(tag); 203 | messageSegmentInputs.add(markupEndSegmentInput); 204 | strPos = globalMatcher.end(); 205 | } 206 | } 207 | } 208 | } 209 | 210 | // Take care of any text that comes after the last match. 211 | if (strPos < inputText.length()) { 212 | String trailingText = inputText.substring(strPos, inputText.length()); 213 | addTextSegment(messageSegmentInputs, trailingText); 214 | } 215 | 216 | return messageSegmentInputs; 217 | } 218 | 219 | private static void addTextSegment(List messageSegmentInputs, String text) { 220 | if (text != null && text.length() > 0) { 221 | ConnectApi.TextSegmentInput textSegmentInput = makeTextSegmentInput(text); 222 | messageSegmentInputs.add(textSegmentInput); 223 | } 224 | } 225 | 226 | private static ConnectApi.TextSegmentInput makeTextSegmentInput(String text) { 227 | ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput(); 228 | textSegment.text = text; 229 | return textSegment; 230 | } 231 | 232 | private static ConnectApi.MentionSegmentInput makeMentionSegmentInput(String mentionId) { 233 | ConnectApi.MentionSegmentInput mentionSegment = new ConnectApi.MentionSegmentInput(); 234 | mentionSegment.id = mentionId; 235 | return mentionSegment; 236 | } 237 | 238 | /** 239 | * Create a MarkupBeginSegmentInput corresponding to the tag. Checking whether the tag is 240 | * supported markup should happen before calling this method. 241 | */ 242 | private static ConnectApi.MarkupBeginSegmentInput makeMarkupBeginSegmentInput(String tag) { 243 | ConnectApi.MarkupBeginSegmentInput markupBeginSegment = new ConnectApi.MarkupBeginSegmentInput(); 244 | markupBeginSegment.markupType = supportedMarkup.get(tag.toLowerCase()); 245 | return markupBeginSegment; 246 | } 247 | 248 | /** 249 | * Create a MarkupEndSegmentInput corresponding to the tag. Checking whether the tag is 250 | * supported markup should happen before calling this method. 251 | */ 252 | private static ConnectApi.MarkupEndSegmentInput makeMarkupEndSegmentInput(String tag) { 253 | ConnectApi.MarkupEndSegmentInput markupEndSegment = new ConnectApi.MarkupEndSegmentInput(); 254 | markupEndSegment.markupType = supportedMarkup.get(tag.toLowerCase()); 255 | return markupEndSegment; 256 | } 257 | 258 | private static ConnectApi.InlineImageSegmentInput makeInlineImageSegmentInput(String fileId, String altText) { 259 | ConnectApi.InlineImageSegmentInput inlineImageSegment = new ConnectApi.InlineImageSegmentInput(); 260 | inlineImageSegment.fileId = fileId; 261 | if (String.isNotBlank(altText)) { 262 | inlineImageSegment.altText = altText; 263 | } 264 | return inlineImageSegment; 265 | } 266 | 267 | /** 268 | * Takes an output feed body and returns a message body input that matches it. 269 | * This is useful for when you retrieve a feed item or comment and want to either re-post or edit it. 270 | */ 271 | public static ConnectApi.MessageBodyInput createInputFromBody(ConnectApi.FeedBody body) { 272 | ConnectApi.MessageBodyInput input = new ConnectApi.MessageBodyInput(); 273 | input.messageSegments = new List(); 274 | 275 | for (ConnectApi.MessageSegment segment : body.messageSegments) { 276 | if (segment instanceof ConnectApi.TextSegment) { 277 | ConnectApi.TextSegment textOutput = (ConnectApi.TextSegment) segment; 278 | ConnectApi.TextSegmentInput textInput = new ConnectApi.TextSegmentInput(); 279 | textInput.text = textOutput.text; 280 | input.messageSegments.add(textInput); 281 | } 282 | else if (segment instanceof ConnectApi.MentionSegment) { 283 | ConnectApi.MentionSegment mentionOutput = (ConnectApi.MentionSegment) segment; 284 | ConnectApi.MentionSegmentInput mentionInput = new ConnectApi.MentionSegmentInput(); 285 | mentionInput.id = mentionOutput.record.id; 286 | input.messageSegments.add(mentionInput); 287 | } 288 | else if (segment instanceof ConnectApi.HashtagSegment) { 289 | ConnectApi.HashtagSegment hashtagOutput = (ConnectApi.HashtagSegment) segment; 290 | ConnectApi.HashtagSegmentInput hashtagInput = new ConnectApi.HashtagSegmentInput(); 291 | hashtagInput.tag = hashtagOutput.tag; 292 | input.messageSegments.add(hashtagInput); 293 | } 294 | else if (segment instanceof ConnectApi.LinkSegment) { 295 | ConnectApi.LinkSegment linkOutput = (ConnectApi.LinkSegment) segment; 296 | ConnectApi.LinkSegmentInput linkInput = new ConnectApi.LinkSegmentInput(); 297 | linkInput.url = linkOutput.url; 298 | input.messageSegments.add(linkInput); 299 | } 300 | else if (segment instanceof ConnectApi.MarkupBeginSegment) { 301 | ConnectApi.MarkupBeginSegment markupBeginOutput = (ConnectApi.MarkupBeginSegment) segment; 302 | ConnectApi.MarkupBeginSegmentInput markupBeginInput = new ConnectApi.MarkupBeginSegmentInput(); 303 | markupBeginInput.markupType = markupBeginOutput.markupType; 304 | input.messageSegments.add(markupBeginInput); 305 | } 306 | else if (segment instanceof ConnectApi.MarkupEndSegment) { 307 | ConnectApi.MarkupEndSegment markupEndOutput = (ConnectApi.MarkupEndSegment) segment; 308 | ConnectApi.MarkupEndSegmentInput markupEndInput = new ConnectApi.MarkupEndSegmentInput(); 309 | markupEndInput.markupType = markupEndOutput.markupType; 310 | input.messageSegments.add(markupEndInput); 311 | } 312 | else if (segment instanceof ConnectApi.InlineImageSegment) { 313 | ConnectApi.InlineImageSegment inlineImageOutput = (ConnectApi.InlineImageSegment) segment; 314 | ConnectApi.InlineImageSegmentInput inlineImageInput = new ConnectApi.InlineImageSegmentInput(); 315 | inlineImageInput.fileId = inlineImageOutput.thumbnails.fileId; 316 | inlineImageInput.altText = inlineImageOutput.altText; 317 | input.messageSegments.add(inlineImageInput); 318 | } 319 | else { 320 | // The other segment types are system-generated and have no corresponding input types. 321 | } 322 | 323 | } 324 | return input; 325 | } 326 | 327 | /** 328 | * Takes an output body and returns a feed item input body that matches it. 329 | * This is useful for when you retrieve a feed item and want to either re-post or edit it. 330 | */ 331 | public static ConnectApi.FeedItemInput createFeedItemInputFromBody(ConnectApi.FeedBody body) { 332 | ConnectApi.MessageBodyInput bodyInput = createInputFromBody(body); 333 | 334 | ConnectApi.FeedItemInput input = new ConnectApi.FeedItemInput(); 335 | input.body = bodyInput; 336 | return input; 337 | } 338 | 339 | /** 340 | * Takes an output body and returns a comment input body that matches it. 341 | * This is useful for when you retrieve a comment and want to either re-post or edit it. 342 | */ 343 | public static ConnectApi.CommentInput createCommentInputFromBody(ConnectApi.FeedBody body) { 344 | ConnectApi.MessageBodyInput bodyInput = createInputFromBody(body); 345 | 346 | ConnectApi.CommentInput input = new ConnectApi.CommentInput(); 347 | input.body = bodyInput; 348 | return input; 349 | } 350 | } -------------------------------------------------------------------------------- /src/classes/ConnectApiHelper.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ConnectApiHelperTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2016, salesforce.com, Inc. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the salesforce.com, Inc. nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 25 | OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | /** 29 | * 30 | * Unit tests for ConnectApiHelper. 31 | * 32 | * This class works with API version 36.0 and later. There are separate classes 33 | * that work with v35.0 and earlier. 34 | * 35 | * See https://github.com/forcedotcom/ConnectApiHelper for more information. 36 | * 37 | */ 38 | 39 | @IsTest(SeeAllData=true) 40 | public class ConnectApiHelperTest { 41 | 42 | @IsTest(SeeAllData=true) 43 | static void testInvalidMentionType() { 44 | Boolean exceptionThrown = false; 45 | try { 46 | ConnectApiHelper.postFeedItemWithMentions(null, 'me', '{001x00000000D7m}'); // not a group or user id 47 | } 48 | catch (ConnectApi.ConnectApiException e) { 49 | System.assertEquals('Only user and group IDs may be used in inline mentions.', e.getMessage()); 50 | exceptionThrown = true; 51 | } 52 | System.assert(exceptionThrown); 53 | } 54 | 55 | @IsTest(SeeAllData=true) 56 | static void testNullString() { 57 | Boolean exceptionThrown = false; 58 | try { 59 | List segments = ConnectApiHelper.getMessageSegmentInputs(null); 60 | } 61 | catch (ConnectApiHelper.InvalidParameterException e) { 62 | exceptionThrown = true; 63 | } 64 | System.assert(exceptionThrown); 65 | } 66 | 67 | @IsTest(SeeAllData=true) 68 | static void testEmptyString() { 69 | List segments = ConnectApiHelper.getMessageSegmentInputs(''); 70 | System.assertEquals(0, segments.size()); 71 | } 72 | 73 | @IsTest(SeeAllData=true) 74 | static void testNoMentions() { 75 | String text = 'hey there'; 76 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 77 | 78 | System.assertEquals(1, segments.size()); 79 | System.assert(segments.get(0) instanceof ConnectApi.TextSegmentInput); 80 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(0); 81 | System.assertEquals(text, textSegment.text); 82 | } 83 | 84 | @IsTest(SeeAllData=true) 85 | static void testMentionOnly() { 86 | String mentionId = '005x0000000URNP'; 87 | String text = '{' + mentionId + '}'; 88 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 89 | 90 | System.assertEquals(1, segments.size()); 91 | System.assert(segments.get(0) instanceof ConnectApi.MentionSegmentInput); 92 | ConnectApi.MentionSegmentInput mentionSegment = (ConnectApi.MentionSegmentInput) segments.get(0); 93 | System.assertEquals(mentionId, mentionSegment.id); 94 | } 95 | 96 | @IsTest(SeeAllData=true) 97 | static void testLeadingMention() { 98 | String mentionId = '005x0000000URNPzzz'; 99 | String restOfMessage = ' - how are you?'; 100 | String text = '{' + mentionId + '}' + restOfMessage; 101 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 102 | 103 | System.assertEquals(2, segments.size()); 104 | System.assert(segments.get(0) instanceof ConnectApi.MentionSegmentInput); 105 | System.assert(segments.get(1) instanceof ConnectApi.TextSegmentInput); 106 | 107 | ConnectApi.MentionSegmentInput mentionSegment = (ConnectApi.MentionSegmentInput) segments.get(0); 108 | System.assertEquals(mentionId, mentionSegment.id); 109 | 110 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(1); 111 | System.assertEquals(restOfMessage, textSegment.text); 112 | } 113 | 114 | @IsTest(SeeAllData=true) 115 | static void testTrailingMention() { 116 | String restOfMessage = 'Here we go: '; 117 | String mentionId = '005x0000000URNPzzz'; 118 | String text = restOfMessage + '{' + mentionId + '}'; 119 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 120 | 121 | System.assertEquals(2, segments.size()); 122 | System.assert(segments.get(0) instanceof ConnectApi.TextSegmentInput); 123 | System.assert(segments.get(1) instanceof ConnectApi.MentionSegmentInput); 124 | 125 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(0); 126 | System.assertEquals(restOfMessage, textSegment.text); 127 | 128 | ConnectApi.MentionSegmentInput mentionSegment = (ConnectApi.MentionSegmentInput) segments.get(1); 129 | System.assertEquals(mentionId, mentionSegment.id); 130 | } 131 | 132 | @IsTest(SeeAllData=true) 133 | static void testAdjacentMentions() { 134 | String mentionId = '005x0000000URNPzzz'; 135 | String mentionId2 = '0F9x00000000D7m'; 136 | String text = '{' + mentionId + '}' + '{' + mentionId2 + '}'; 137 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 138 | 139 | System.assertEquals(2, segments.size()); 140 | System.assert(segments.get(0) instanceof ConnectApi.MentionSegmentInput); 141 | System.assert(segments.get(1) instanceof ConnectApi.MentionSegmentInput); 142 | 143 | ConnectApi.MentionSegmentInput mentionSegment = (ConnectApi.MentionSegmentInput) segments.get(0); 144 | System.assertEquals(mentionId, mentionSegment.id); 145 | 146 | ConnectApi.MentionSegmentInput mentionSegment2 = (ConnectApi.MentionSegmentInput) segments.get(1); 147 | System.assertEquals(mentionId2, mentionSegment2.id); 148 | } 149 | 150 | @IsTest(SeeAllData=true) 151 | static void testLinkAndHashtagParsing() { 152 | // The test string is: #Yolo: http://salesforce.com, {005} {005x0000000URNPzzz} test. 153 | // [ ][][ ][ ][ ][ ] 154 | // 0 1 2 3 4 5 155 | // 0 = hashtag 156 | // 1 = text1 157 | // 2 = link 158 | // 3 = text2 159 | // 4 = mention 160 | // 5 = text3 161 | 162 | String hashtag = 'Yolo'; 163 | String text1 = ': '; 164 | String link = 'http://salesforce.com'; 165 | String text2 = ', {005} '; 166 | String mentionId = UserInfo.getUserId(); 167 | String text3 = ' test.'; 168 | String text = '#' + hashtag + text1 + link + text2 + '{' + mentionId + '}' + text3; 169 | 170 | ConnectApi.FeedElement fi = ConnectApiHelper.postFeedItemWithRichText(null, 'me', text); 171 | 172 | List segments = fi.body.messageSegments; 173 | 174 | System.assertEquals(6, segments.size()); 175 | System.assert(segments.get(0) instanceof ConnectApi.HashtagSegment); 176 | System.assert(segments.get(1) instanceof ConnectApi.TextSegment); 177 | System.assert(segments.get(2) instanceof ConnectApi.LinkSegment); 178 | System.assert(segments.get(3) instanceof ConnectApi.TextSegment); 179 | System.assert(segments.get(4) instanceof ConnectApi.MentionSegment); 180 | System.assert(segments.get(5) instanceof ConnectApi.TextSegment); 181 | 182 | ConnectApi.HashtagSegment hashtagSegment = (ConnectApi.HashtagSegment) segments.get(0); 183 | System.assertEquals(hashtag, hashtagSegment.tag); 184 | 185 | ConnectApi.TextSegment textSegment1 = (ConnectApi.TextSegment) segments.get(1); 186 | System.assertEquals(text1, textSegment1.text); 187 | 188 | ConnectApi.LinkSegment linkSegment = (ConnectApi.LinkSegment) segments.get(2); 189 | System.assertEquals(link, linkSegment.url); 190 | 191 | ConnectApi.TextSegment textSegment2 = (ConnectApi.TextSegment) segments.get(3); 192 | System.assertEquals(text2, textSegment2.text); 193 | 194 | ConnectApi.MentionSegment mentionSegment = (ConnectApi.MentionSegment) segments.get(4); 195 | System.assertEquals(mentionId, mentionSegment.record.id); 196 | 197 | ConnectApi.TextSegment textSegment3 = (ConnectApi.TextSegment) segments.get(5); 198 | System.assertEquals(text3, textSegment3.text); 199 | } 200 | 201 | @IsTest(SeeAllData=true) 202 | static void testMentionInComment() { 203 | ConnectApi.FeedElement fi = ConnectApi.ChatterFeeds.postFeedElement(null, 'me', ConnectApi.FeedElementType.FeedItem, 'hi'); 204 | String mentionId = UserInfo.getUserId(); 205 | String text = '{' + mentionId + '}'; 206 | ConnectApi.Comment comment = ConnectApiHelper.postCommentWithMentions(null, fi.id, text); 207 | 208 | List segments = comment.body.messageSegments; 209 | System.assertEquals(1, segments.size()); 210 | System.assert(segments.get(0) instanceof ConnectApi.MentionSegment); 211 | ConnectApi.MentionSegment mentionSegment = (ConnectApi.MentionSegment) segments.get(0); 212 | System.assertEquals(mentionId, mentionSegment.record.id); 213 | } 214 | 215 | @IsTest(SeeAllData=true) 216 | static void testCreateInputFromBody() { 217 | 218 | // We'll post a feed item that contains text, link, hashtag, mention, and markup segments, 219 | // and then call the helper method on the resulting body. 220 | 221 | ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput(); 222 | 223 | ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput(); 224 | messageBodyInput.messageSegments = new List(); 225 | 226 | // We can put the link and hashtag parts into a text segment to post the feed item. When it gets retrieved, it will have 227 | // separate segments for the text, link and hashtag. 228 | String expectedText = 'Text '; 229 | String expectedLink = 'http://link.com'; 230 | String expectedHashtag = 'hashtag'; 231 | String expectedBoldText = 'Bold text'; 232 | 233 | ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput(); 234 | textSegmentInput.text = expectedText + expectedLink + ' #' + expectedHashtag; 235 | messageBodyInput.messageSegments.add(textSegmentInput); 236 | 237 | ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput(); 238 | mentionSegmentInput.id = UserInfo.getUserId(); 239 | messageBodyInput.messageSegments.add(mentionSegmentInput); 240 | 241 | ConnectApi.MarkupBeginSegmentInput markupBeginSegmentInput = new ConnectApi.MarkupBeginSegmentInput(); 242 | markupBeginSegmentInput.markupType = ConnectApi.MarkupType.Bold; 243 | messageBodyInput.messageSegments.add(markupBeginSegmentInput); 244 | 245 | textSegmentInput = new ConnectApi.TextSegmentInput(); 246 | textSegmentInput.text = expectedBoldText; 247 | messageBodyInput.messageSegments.add(textSegmentInput); 248 | 249 | ConnectApi.MarkupEndSegmentInput markupEndSegmentInput = new ConnectApi.MarkupEndSegmentInput(); 250 | markupEndSegmentInput.markupType = ConnectApi.MarkupType.Bold; 251 | messageBodyInput.messageSegments.add(markupEndSegmentInput); 252 | 253 | feedItemInput.body = messageBodyInput; 254 | feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem; 255 | feedItemInput.subjectId = UserInfo.getUserId(); 256 | 257 | ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput); 258 | 259 | ConnectApi.MessageBodyInput input = ConnectApiHelper.createInputFromBody(feedElement.body); 260 | System.assertEquals(8, input.messageSegments.size(), 'Wrong number of message segments.'); 261 | 262 | System.assert(input.messageSegments.get(0) instanceof ConnectApi.TextSegmentInput, 'Segment 0 is not a text segment input.'); 263 | ConnectApi.TextSegmentInput textInput = (ConnectApi.TextSegmentInput) input.messageSegments.get(0); 264 | System.assertEquals(expectedText, textInput.text, 'Segment 0 text does not match.'); 265 | 266 | System.assert(input.messageSegments.get(1) instanceof ConnectApi.LinkSegmentInput, 'Segment 1 is not a link segment input.'); 267 | ConnectApi.LinkSegmentInput linkInput = (ConnectApi.LinkSegmentInput) input.messageSegments.get(1); 268 | System.assertEquals(expectedLink, linkInput.url, 'Segment 1 url does not match.'); 269 | 270 | System.assert(input.messageSegments.get(2) instanceof ConnectApi.TextSegmentInput, 'Segment 2 is not a text segment input.'); 271 | ConnectApi.TextSegmentInput textInput2 = (ConnectApi.TextSegmentInput) input.messageSegments.get(2); 272 | System.assertEquals(' ', textInput2.text, 'Segment 2 text does not match.'); 273 | 274 | System.assert(input.messageSegments.get(3) instanceof ConnectApi.HashtagSegmentInput, 'Segment 3 is not a hashtag segment input.'); 275 | ConnectApi.HashtagSegmentInput hashtagInput = (ConnectApi.HashtagSegmentInput) input.messageSegments.get(3); 276 | System.assertEquals(expectedHashtag, hashtagInput.tag, 'Segment 3 hashtag does not match.'); 277 | 278 | System.assert(input.messageSegments.get(4) instanceof ConnectApi.MentionSegmentInput, 'Segment 4 is not a mention segment input.'); 279 | ConnectApi.MentionSegmentInput mentionInput = (ConnectApi.MentionSegmentInput) input.messageSegments.get(4); 280 | System.assertEquals(UserInfo.getUserId(), mentionInput.id, 'Segment 4 mention ID does not match.'); 281 | 282 | System.assert(input.messageSegments.get(5) instanceof ConnectApi.MarkupBeginSegmentInput, 'Segment 5 is not a markup begin segment input.'); 283 | ConnectApi.MarkupBeginSegmentInput markupBeginInput = (ConnectApi.MarkupBeginSegmentInput) input.messageSegments.get(5); 284 | System.assertEquals(ConnectApi.MarkupType.Bold, markupBeginInput.markupType, 'Segment 5 markup type does not match.'); 285 | 286 | System.assert(input.messageSegments.get(6) instanceof ConnectApi.TextSegmentInput, 'Segment 6 is not a text segment input.'); 287 | ConnectApi.TextSegmentInput textInput3 = (ConnectApi.TextSegmentInput) input.messageSegments.get(6); 288 | System.assertEquals(expectedBoldText, textInput3.text, 'Segment 6 text does not match.'); 289 | 290 | System.assert(input.messageSegments.get(7) instanceof ConnectApi.MarkupEndSegmentInput, 'Segment 7 is not a markup end segment input.'); 291 | ConnectApi.MarkupEndSegmentInput markupEndInput = (ConnectApi.MarkupEndSegmentInput) input.messageSegments.get(7); 292 | System.assertEquals(ConnectApi.MarkupType.Bold, markupEndInput.markupType, 'Segment 7 markup type does not match.'); 293 | 294 | // Get coverage for the createFeedItemInputFromBody() method. 295 | ConnectApi.FeedItemInput feedItemInput2 = ConnectApiHelper.createFeedItemInputFromBody(feedElement.body); 296 | System.assertEquals(input, feedItemInput2.body, 'createFeedItemInputFromBody is returning a different input body than createInputFromBody.'); 297 | 298 | // Get coverage for the createCommentInputFromBody() method. 299 | ConnectApi.CommentInput commentInput = ConnectApiHelper.createCommentInputFromBody(feedElement.body); 300 | System.assertEquals(input, commentInput.body, 'createCommentInputFromBody is returning a different input body than createInputFromBody.'); 301 | } 302 | 303 | @IsTest(SeeAllData=true) 304 | static void testCreateInputFromBodyWithGeneratedSegment() { 305 | ConnectApi.FeedBody body = new ConnectApi.FeedBody(); 306 | body.messageSegments = new List(); 307 | 308 | // Mock up an entity link segment. 309 | ConnectApi.EntityLinkSegment entityLinkSegment = new ConnectApi.EntityLinkSegment(); 310 | entityLinkSegment.text = 'blah'; 311 | 312 | body.messageSegments.add(entityLinkSegment); 313 | body.text = 'blah'; 314 | 315 | ConnectApi.MessageBodyInput input = ConnectApiHelper.createInputFromBody(body); 316 | System.assertEquals(0, input.messageSegments.size(), 'Wrong number of message segments.'); 317 | } 318 | 319 | 320 | @IsTest(SeeAllData=true) 321 | static void testUnsupportedMarkup() { 322 | // a,

      b
      Does this work?

      323 | // [0 ][1][2 ][3 ][4 ] 324 | // 0 = text1 325 | // 1 = markup begin 326 | // 2 = text2 327 | // 3 = markup end 328 | // 4 = text3 329 | String text1 = 'a,

      b
      '; 330 | String text2 = 'Does this work?'; 331 | String text3 = '

      '; 332 | String text = text1 + '' + text2 + '' + text3; 333 | 334 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 335 | 336 | System.assertEquals(5, segments.size()); 337 | System.assert(segments.get(0) instanceof ConnectApi.TextSegmentInput); 338 | System.assert(segments.get(1) instanceof ConnectApi.MarkupBeginSegmentInput); 339 | System.assert(segments.get(2) instanceof ConnectApi.TextSegmentInput); 340 | System.assert(segments.get(3) instanceof ConnectApi.MarkupEndSegmentInput); 341 | System.assert(segments.get(4) instanceof ConnectApi.TextSegmentInput); 342 | 343 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(0); 344 | System.assertEquals(text1, textSegment.text); 345 | 346 | ConnectApi.MarkupBeginSegmentInput markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(1); 347 | System.assertEquals(ConnectApi.MarkupType.Bold, markupBeginSegment.markupType); 348 | 349 | textSegment = (ConnectApi.TextSegmentInput) segments.get(2); 350 | System.assertEquals(text2, textSegment.text); 351 | 352 | ConnectApi.MarkupEndSegmentInput markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(3); 353 | System.assertEquals(ConnectApi.MarkupType.Bold, markupEndSegment.markupType); 354 | 355 | textSegment = (ConnectApi.TextSegmentInput) segments.get(4); 356 | System.assertEquals(text3, textSegment.text); 357 | } 358 | 359 | @IsTest(SeeAllData=true) 360 | static void testSimpleMarkup() { 361 | String restOfMessage = 'blah'; 362 | String text = '' + restOfMessage + ''; 363 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 364 | 365 | System.assertEquals(3, segments.size()); 366 | System.assert(segments.get(0) instanceof ConnectApi.MarkupBeginSegmentInput); 367 | System.assert(segments.get(1) instanceof ConnectApi.TextSegmentInput); 368 | System.assert(segments.get(2) instanceof ConnectApi.MarkupEndSegmentInput); 369 | 370 | ConnectApi.MarkupBeginSegmentInput markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(0); 371 | System.assertEquals(ConnectApi.MarkupType.Underline, markupBeginSegment.markupType); 372 | 373 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(1); 374 | System.assertEquals(restOfMessage, textSegment.text); 375 | 376 | ConnectApi.MarkupEndSegmentInput markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(2); 377 | System.assertEquals(ConnectApi.MarkupType.Underline, markupEndSegment.markupType); 378 | } 379 | 380 | @IsTest(SeeAllData=true) 381 | static void testMarkupCasing() { 382 | String text1 = 'foo'; 383 | String text2 = 'bar'; 384 | String text3 = 'baz'; 385 | String text = '' + text1 + '' + text2 + '
      1. ' + text3 + '
      '; 386 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 387 | 388 | System.assertEquals(11, segments.size()); 389 | System.assert(segments.get(0) instanceof ConnectApi.MarkupBeginSegmentInput); 390 | System.assert(segments.get(1) instanceof ConnectApi.TextSegmentInput); 391 | System.assert(segments.get(2) instanceof ConnectApi.MarkupEndSegmentInput); 392 | System.assert(segments.get(3) instanceof ConnectApi.MarkupBeginSegmentInput); 393 | System.assert(segments.get(4) instanceof ConnectApi.TextSegmentInput); 394 | System.assert(segments.get(5) instanceof ConnectApi.MarkupEndSegmentInput); 395 | System.assert(segments.get(6) instanceof ConnectApi.MarkupBeginSegmentInput); 396 | System.assert(segments.get(7) instanceof ConnectApi.MarkupBeginSegmentInput); 397 | System.assert(segments.get(8) instanceof ConnectApi.TextSegmentInput); 398 | System.assert(segments.get(9) instanceof ConnectApi.MarkupEndSegmentInput); 399 | System.assert(segments.get(10) instanceof ConnectApi.MarkupEndSegmentInput); 400 | 401 | ConnectApi.MarkupBeginSegmentInput markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(0); 402 | System.assertEquals(ConnectApi.MarkupType.Underline, markupBeginSegment.markupType); 403 | 404 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(1); 405 | System.assertEquals(text1, textSegment.text); 406 | 407 | ConnectApi.MarkupEndSegmentInput markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(2); 408 | System.assertEquals(ConnectApi.MarkupType.Underline, markupEndSegment.markupType); 409 | 410 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(3); 411 | System.assertEquals(ConnectApi.MarkupType.Bold, markupBeginSegment.markupType); 412 | 413 | textSegment = (ConnectApi.TextSegmentInput) segments.get(4); 414 | System.assertEquals(text2, textSegment.text); 415 | 416 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(5); 417 | System.assertEquals(ConnectApi.MarkupType.Bold, markupEndSegment.markupType); 418 | 419 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(6); 420 | System.assertEquals(ConnectApi.MarkupType.OrderedList, markupBeginSegment.markupType); 421 | 422 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(7); 423 | System.assertEquals(ConnectApi.MarkupType.ListItem, markupBeginSegment.markupType); 424 | 425 | textSegment = (ConnectApi.TextSegmentInput) segments.get(8); 426 | System.assertEquals(text3, textSegment.text); 427 | 428 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(9); 429 | System.assertEquals(ConnectApi.MarkupType.ListItem, markupEndSegment.markupType); 430 | 431 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(10); 432 | System.assertEquals(ConnectApi.MarkupType.OrderedList, markupEndSegment.markupType); 433 | } 434 | 435 | @IsTest(SeeAllData=true) 436 | static void testInlineImage() { 437 | String restOfMessage = 'Check out this image!'; 438 | String imageId = '069B0000000q7hi'; 439 | String altText = 'Some alt text.'; 440 | String text = restOfMessage + '{img:' + imageId + ':' + altText + '}'; 441 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 442 | 443 | System.assertEquals(2, segments.size()); 444 | System.assert(segments.get(0) instanceof ConnectApi.TextSegmentInput); 445 | System.assert(segments.get(1) instanceof ConnectApi.InlineImageSegmentInput); 446 | 447 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(0); 448 | System.assertEquals(restOfMessage, textSegment.text); 449 | 450 | ConnectApi.InlineImageSegmentInput inlineImageSegment = (ConnectApi.InlineImageSegmentInput) segments.get(1); 451 | System.assertEquals(imageId, inlineImageSegment.fileId); 452 | System.assertEquals(altText, inlineImageSegment.altText); 453 | } 454 | 455 | @IsTest(SeeAllData=true) 456 | static void testInlineImageNoAltText() { 457 | String imageId = '069B0000000q7hi'; 458 | String text = '{img:' + imageId + '}'; 459 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 460 | 461 | System.assertEquals(1, segments.size()); 462 | System.assert(segments.get(0) instanceof ConnectApi.InlineImageSegmentInput); 463 | 464 | ConnectApi.InlineImageSegmentInput inlineImageSegment = (ConnectApi.InlineImageSegmentInput) segments.get(0); 465 | System.assertEquals(imageId, inlineImageSegment.fileId); 466 | System.assertEquals(null, inlineImageSegment.altText); 467 | } 468 | 469 | @IsTest(SeeAllData=true) 470 | static void testInlineImageAltTextSyntax() { 471 | String imageId15 = '069B0000000q7hi'; 472 | String imageId18 = '069B0000000q7hixxx'; 473 | String altText1 = 'Alt text with a colon : in the middle.'; 474 | String badSyntax1 = 'Alt text with a closing brace '; 475 | String badSyntax2 = ' in the middle.}'; 476 | String badSyntaxAltText = badSyntax1 + '}' + badSyntax2; 477 | String text = '{img:' + imageId15 + ':' + altText1 + '}{img:' + imageId18 + ':}{img:' + imageId15 + ':' + badSyntaxAltText; 478 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 479 | 480 | System.assertEquals(4, segments.size()); 481 | System.assert(segments.get(0) instanceof ConnectApi.InlineImageSegmentInput); 482 | System.assert(segments.get(1) instanceof ConnectApi.InlineImageSegmentInput); 483 | System.assert(segments.get(2) instanceof ConnectApi.InlineImageSegmentInput); 484 | System.assert(segments.get(3) instanceof ConnectApi.TextSegmentInput); 485 | 486 | ConnectApi.InlineImageSegmentInput inlineImageSegment = (ConnectApi.InlineImageSegmentInput) segments.get(0); 487 | System.assertEquals(imageId15, inlineImageSegment.fileId); 488 | System.assertEquals(altText1, inlineImageSegment.altText); 489 | 490 | inlineImageSegment = (ConnectApi.InlineImageSegmentInput) segments.get(1); 491 | System.assertEquals(imageId18, inlineImageSegment.fileId); 492 | System.assertEquals(null, inlineImageSegment.altText); 493 | 494 | inlineImageSegment = (ConnectApi.InlineImageSegmentInput) segments.get(2); 495 | System.assertEquals(imageId15, inlineImageSegment.fileId); 496 | System.assertEquals(badSyntax1, inlineImageSegment.altText); 497 | 498 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(3); 499 | System.assertEquals(badSyntax2, textSegment.text); 500 | } 501 | 502 | @IsTest(SeeAllData=true) 503 | static void testMarkupAndMention() { 504 | String mentionId = '005x0000000URNPzzz'; 505 | String message = 'How are you'; 506 | String questionMark = '?'; 507 | String text = '' + message + '{' + mentionId + '}' + questionMark + ''; 508 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 509 | 510 | System.assertEquals(7, segments.size()); 511 | System.assert(segments.get(0) instanceof ConnectApi.MarkupBeginSegmentInput); 512 | System.assert(segments.get(1) instanceof ConnectApi.TextSegmentInput); 513 | System.assert(segments.get(2) instanceof ConnectApi.MarkupBeginSegmentInput); 514 | System.assert(segments.get(3) instanceof ConnectApi.MentionSegmentInput); 515 | System.assert(segments.get(4) instanceof ConnectApi.MarkupEndSegmentInput); 516 | System.assert(segments.get(5) instanceof ConnectApi.TextSegmentInput); 517 | System.assert(segments.get(6) instanceof ConnectApi.MarkupEndSegmentInput); 518 | 519 | ConnectApi.MarkupBeginSegmentInput markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(0); 520 | System.assertEquals(ConnectApi.MarkupType.Bold, markupBeginSegment.markupType); 521 | 522 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(1); 523 | System.assertEquals(message, textSegment.text); 524 | 525 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(2); 526 | System.assertEquals(ConnectApi.MarkupType.Italic, markupBeginSegment.markupType); 527 | 528 | ConnectApi.MentionSegmentInput mentionSegment = (ConnectApi.MentionSegmentInput) segments.get(3); 529 | System.assertEquals(mentionId, mentionSegment.id); 530 | 531 | ConnectApi.MarkupEndSegmentInput markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(4); 532 | System.assertEquals(ConnectApi.MarkupType.Italic, markupEndSegment.markupType); 533 | 534 | textSegment = (ConnectApi.TextSegmentInput) segments.get(5); 535 | System.assertEquals(questionMark, textSegment.text); 536 | 537 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(6); 538 | System.assertEquals(ConnectApi.MarkupType.Bold, markupEndSegment.markupType); 539 | } 540 | 541 | @IsTest(SeeAllData=true) 542 | static void testAllMarkupAndInlineImage() { 543 | //

      This is an italicized paragraph.

      544 | //
      • A completed item in an unordered list.
      545 | //
      1. An underlined item in an ordered list.
      546 | // And, of course, an image for you: {img:069B0000000q7hi:An image of something nice.} 547 | String text1 = 'This is an italicized paragraph.'; 548 | String text2 = 'A completed item in an unordered list.'; 549 | String text3 = 'An underlined item in an ordered list.'; 550 | String text4 = 'And, of course, an image for you: '; 551 | String text5 = ' '; 552 | String imageId = '069B0000000q7hi'; 553 | String altText = 'An image of something nice.'; 554 | String text = '

      ' + text1 + '

      • ' + text2 + '
      1. ' 555 | + text3 + '
      ' + text4 + ' {img:' + imageId + ':' + altText + '}'; 556 | List segments = ConnectApiHelper.getMessageSegmentInputs(text); 557 | 558 | System.assertEquals(24, segments.size()); 559 | //

      This is an italicized paragraph.

      560 | System.assert(segments.get(0) instanceof ConnectApi.MarkupBeginSegmentInput); 561 | System.assert(segments.get(1) instanceof ConnectApi.MarkupBeginSegmentInput); 562 | System.assert(segments.get(2) instanceof ConnectApi.TextSegmentInput); 563 | System.assert(segments.get(3) instanceof ConnectApi.MarkupEndSegmentInput); 564 | System.assert(segments.get(4) instanceof ConnectApi.MarkupEndSegmentInput); 565 | 566 | //
      • A completed item in an unordered list.
      567 | System.assert(segments.get(5) instanceof ConnectApi.MarkupBeginSegmentInput); 568 | System.assert(segments.get(6) instanceof ConnectApi.MarkupBeginSegmentInput); 569 | System.assert(segments.get(7) instanceof ConnectApi.MarkupBeginSegmentInput); 570 | System.assert(segments.get(8) instanceof ConnectApi.TextSegmentInput); 571 | System.assert(segments.get(9) instanceof ConnectApi.MarkupEndSegmentInput); 572 | System.assert(segments.get(10) instanceof ConnectApi.MarkupEndSegmentInput); 573 | System.assert(segments.get(11) instanceof ConnectApi.MarkupEndSegmentInput); 574 | 575 | //
      1. An underlined item in an ordered list.
      576 | System.assert(segments.get(12) instanceof ConnectApi.MarkupBeginSegmentInput); 577 | System.assert(segments.get(13) instanceof ConnectApi.MarkupBeginSegmentInput); 578 | System.assert(segments.get(14) instanceof ConnectApi.MarkupBeginSegmentInput); 579 | System.assert(segments.get(15) instanceof ConnectApi.TextSegmentInput); 580 | System.assert(segments.get(16) instanceof ConnectApi.MarkupEndSegmentInput); 581 | System.assert(segments.get(17) instanceof ConnectApi.MarkupEndSegmentInput); 582 | System.assert(segments.get(18) instanceof ConnectApi.MarkupEndSegmentInput); 583 | 584 | // And, of course, an image for you: {img:069B0000000q7hi:An image of something nice.} 585 | System.assert(segments.get(19) instanceof ConnectApi.MarkupBeginSegmentInput); 586 | System.assert(segments.get(20) instanceof ConnectApi.TextSegmentInput); 587 | System.assert(segments.get(21) instanceof ConnectApi.MarkupEndSegmentInput); 588 | System.assert(segments.get(22) instanceof ConnectApi.TextSegmentInput); 589 | System.assert(segments.get(23) instanceof ConnectApi.InlineImageSegmentInput); 590 | 591 | //

      This is an italicized paragraph.

      592 | ConnectApi.MarkupBeginSegmentInput markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(0); 593 | System.assertEquals(ConnectApi.MarkupType.Paragraph, markupBeginSegment.markupType); 594 | 595 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(1); 596 | System.assertEquals(ConnectApi.MarkupType.Italic, markupBeginSegment.markupType); 597 | 598 | ConnectApi.TextSegmentInput textSegment = (ConnectApi.TextSegmentInput) segments.get(2); 599 | System.assertEquals(text1, textSegment.text); 600 | 601 | ConnectApi.MarkupEndSegmentInput markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(3); 602 | System.assertEquals(ConnectApi.MarkupType.Italic, markupEndSegment.markupType); 603 | 604 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(4); 605 | System.assertEquals(ConnectApi.MarkupType.Paragraph, markupEndSegment.markupType); 606 | 607 | //
      • A completed item in an unordered list.
      608 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(5); 609 | System.assertEquals(ConnectApi.MarkupType.UnorderedList, markupBeginSegment.markupType); 610 | 611 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(6); 612 | System.assertEquals(ConnectApi.MarkupType.ListItem, markupBeginSegment.markupType); 613 | 614 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(7); 615 | System.assertEquals(ConnectApi.MarkupType.Strikethrough, markupBeginSegment.markupType); 616 | 617 | textSegment = (ConnectApi.TextSegmentInput) segments.get(8); 618 | System.assertEquals(text2, textSegment.text); 619 | 620 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(9); 621 | System.assertEquals(ConnectApi.MarkupType.Strikethrough, markupEndSegment.markupType); 622 | 623 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(10); 624 | System.assertEquals(ConnectApi.MarkupType.ListItem, markupEndSegment.markupType); 625 | 626 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(11); 627 | System.assertEquals(ConnectApi.MarkupType.UnorderedList, markupEndSegment.markupType); 628 | 629 | //
      1. An underlined item in an ordered list.
      630 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(12); 631 | System.assertEquals(ConnectApi.MarkupType.OrderedList, markupBeginSegment.markupType); 632 | 633 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(13); 634 | System.assertEquals(ConnectApi.MarkupType.ListItem, markupBeginSegment.markupType); 635 | 636 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(14); 637 | System.assertEquals(ConnectApi.MarkupType.Underline, markupBeginSegment.markupType); 638 | 639 | textSegment = (ConnectApi.TextSegmentInput) segments.get(15); 640 | System.assertEquals(text3, textSegment.text); 641 | 642 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(16); 643 | System.assertEquals(ConnectApi.MarkupType.Underline, markupEndSegment.markupType); 644 | 645 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(17); 646 | System.assertEquals(ConnectApi.MarkupType.ListItem, markupEndSegment.markupType); 647 | 648 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(18); 649 | System.assertEquals(ConnectApi.MarkupType.OrderedList, markupEndSegment.markupType); 650 | 651 | // And, of course, an image for you: {img:069B0000000q7hi:An image of something nice.} 652 | markupBeginSegment = (ConnectApi.MarkupBeginSegmentInput) segments.get(19); 653 | System.assertEquals(ConnectApi.MarkupType.Bold, markupBeginSegment.markupType); 654 | 655 | textSegment = (ConnectApi.TextSegmentInput) segments.get(20); 656 | System.assertEquals(text4, textSegment.text); 657 | 658 | markupEndSegment = (ConnectApi.MarkupEndSegmentInput) segments.get(21); 659 | System.assertEquals(ConnectApi.MarkupType.Bold, markupEndSegment.markupType); 660 | 661 | textSegment = (ConnectApi.TextSegmentInput) segments.get(22); 662 | System.assertEquals(text5, textSegment.text); 663 | 664 | ConnectApi.InlineImageSegmentInput inlineImageSegment = (ConnectApi.InlineImageSegmentInput) segments.get(23); 665 | System.assertEquals(imageId, inlineImageSegment.fileId); 666 | System.assertEquals(altText, inlineImageSegment.altText); 667 | } 668 | } -------------------------------------------------------------------------------- /src/classes/ConnectApiHelperTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/email/Chatter_Bot_Email_Templates-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | Chatter Bot Email Templates 5 | ReadOnly 6 | 7 | -------------------------------------------------------------------------------- /src/email/Chatter_Bot_Email_Templates/Chatter_Bot_Post_Message_Template.email: -------------------------------------------------------------------------------- 1 |

      2 | Congratulations, you've just received a Chatter post by Chatter Bot for joining group {{!Chatter_Bot_Group_Member__c.Chatter_Group_ID__c}} 3 |

       

      4 | Here are some things to know: 5 |

      6 | 7 |

       

      8 | 9 |

      10 | @ mentions 11 |

        12 |
      1. 13 | Chatter messages support @ mentions by placing a Chatter User, Group, or Record ID between two curly brackets like this {userId}. 14 |
      2. 15 |
      3. 16 | When working with merge fields in an Email Template, the Object and Field names are enclosed between '{!' and '}' so you would still need to add one more set of { and } around that. 17 |
      4. 18 |
      5. 19 | Here's an example where we are @ mentioning your name {{!Chatter_Bot_Group_Member__c.MemberId__c}} 20 |
      6. 21 |
      22 |

      23 | 24 |

       

      25 | 26 |

      27 | Rich Text Formatting 28 |

        29 |
      • 30 | Chatter messages also support a subset of HTML tags to make rich text posts. 31 |
      • 32 |
      • 33 | <p> starts a new paragraph </p> 34 |
      • 35 |
      • 36 | <b> bold </b> 37 |
      • 38 |
      • 39 | <i> italic </i> 40 |
      • 41 |
      • 42 | <s> strikethrough </s> 43 |
      • 44 |
      • 45 | <u> underline </u> 46 |
      • 47 |
      • 48 | <ul> bulleted list </ul> 49 |
      • 50 |
      • 51 | <ol> numbered list </ol> 52 |
      • 53 |
      • 54 | <li> list item within a bulleted or numbered list </li> 55 |
      • 56 |
      57 |

      58 | 59 |

       

      60 | 61 |

      62 | Blank Lines 63 |

      64 | Since Chatter does not support the <br> break tag then you can use <p>&nbsp;</p> to create a blank paragraph with same effect. 65 |

      66 |

      -------------------------------------------------------------------------------- /src/email/Chatter_Bot_Email_Templates/Chatter_Bot_Post_Message_Template.email-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Example template for posting Chatter messages as specific user. 5 | UTF-8 6 | Chatter Bot Post Message Template 7 | 8 | Chatter Bot Post Message Template Subject Is Ignored 9 | <p> 10 | Congratulations, you've just received a Chatter post by Chatter Bot! 11 | <p>&nbsp;</p> 12 | Here are some things to know: 13 | </p> 14 | 15 | <p>&nbsp;</p> 16 | 17 | <p> 18 | <b>@ mentions</b> 19 | <ol> 20 | <li> 21 | Chatter messages support @ mentions by placing a Chatter User, Group, or Record ID between two curly brackets like this {userId}. 22 | </li> 23 | <li> 24 | When working with merge fields in an Email Template, the Object and Field names are enclosed between '{!' and '}' so you would still need to add one more set of { and } around that. 25 | </li> 26 | <li> 27 | Here's an example where we are @ mentioning your name {{!Chatter_Bot_Group_Member__c.MemberId__c}} 28 | </li> 29 | </ol> 30 | </p> 31 | 32 | <p>&nbsp;</p> 33 | 34 | <p> 35 | <b>Rich Text Formatting</b> 36 | <ul> 37 | <li> 38 | Chatter messages also support a subset of HTML tags to make rich text posts. 39 | </li> 40 | <li> 41 | &lt;p&gt; starts a new paragraph &lt;/p&gt; 42 | </li> 43 | <li> 44 | &lt;b&gt; <b>bold</b> &lt;/b&gt; 45 | </li> 46 | <li> 47 | &lt;i&gt; <i>italic</i> &lt;/i&gt; 48 | </li> 49 | <li> 50 | &lt;s&gt; <s>strikethrough</s> &lt;/s&gt; 51 | </li> 52 | <li> 53 | &lt;u&gt; <u>underline</u> &lt;/u&gt; 54 | </li> 55 | <li> 56 | &lt;ul&gt; bulleted list &lt;/ul&gt; 57 | </li> 58 | <li> 59 | &lt;ol&gt; numbered list &lt;/ol&gt; 60 | </li> 61 | <li> 62 | &lt;li&gt; list item within a bulleted or numbered list &lt;/li&gt; 63 | </li> 64 | </ul> 65 | </p> 66 | 67 | <p>&nbsp;</p> 68 | 69 | <p> 70 | <b>Blank Lines</b> 71 | <p> 72 | Since Chatter does not support the &lt;br&gt; break tag then you can use &lt;p&gt;&amp;nbsp;&lt;/p&gt; to create a blank paragraph with same effect. 73 | </p> 74 | </p> 75 | custom 76 | 77 | -------------------------------------------------------------------------------- /src/flowDefinitions/Chatter_Bot_Welcome_New_Group_Member.flowDefinition: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 4 | 5 | -------------------------------------------------------------------------------- /src/flows/Chatter_Bot_Welcome_New_Group_Member-6.flow: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | apexSelection 6 | 7 | CB: Post Message 8 | 9 | 10 | myRule_1_A1 11 | 12 | 100 13 | 200 14 | ChatterBotPostMessageInvocable 15 | apex 16 | 17 | 18 | dataType 19 | 20 | String 21 | 22 | 23 | 24 | isRequired 25 | 26 | true 27 | 28 | 29 | 30 | leftHandSideLabel 31 | 32 | Author User ID 33 | 34 | 35 | 36 | maxOccurs 37 | 38 | 0.0 39 | 40 | 41 | 42 | objectType 43 | 44 | 45 | 46 | 47 | 48 | rightHandSideType 49 | 50 | Formula 51 | 52 | 53 | authorId 54 | 55 | formula_2_myRule_1_A1_authorId 56 | 57 | 58 | 59 | 60 | dataType 61 | 62 | String 63 | 64 | 65 | 66 | isRequired 67 | 68 | true 69 | 70 | 71 | 72 | leftHandSideLabel 73 | 74 | User, Group, or Record ID 75 | 76 | 77 | 78 | maxOccurs 79 | 80 | 0.0 81 | 82 | 83 | 84 | objectType 85 | 86 | 87 | 88 | 89 | 90 | rightHandSideType 91 | 92 | Reference 93 | 94 | 95 | subjectId 96 | 97 | myVariable_current.Member__c 98 | 99 | 100 | 101 | 102 | dataType 103 | 104 | String 105 | 106 | 107 | 108 | isRequired 109 | 110 | false 111 | 112 | 113 | 114 | leftHandSideLabel 115 | 116 | Email Template Unique Name 117 | 118 | 119 | 120 | maxOccurs 121 | 122 | 1.0 123 | 124 | 125 | 126 | objectType 127 | 128 | 129 | 130 | 131 | 132 | rightHandSideType 133 | 134 | String 135 | 136 | 137 | emailTemplateName 138 | 139 | Chatter_Bot_Post_Message_Template 140 | 141 | 142 | 143 | 144 | dataType 145 | 146 | String 147 | 148 | 149 | 150 | isRequired 151 | 152 | false 153 | 154 | 155 | 156 | leftHandSideLabel 157 | 158 | Record ID (Template Merge Fields) 159 | 160 | 161 | 162 | maxOccurs 163 | 164 | 1.0 165 | 166 | 167 | 168 | objectType 169 | 170 | 171 | 172 | 173 | 174 | rightHandSideType 175 | 176 | Reference 177 | 178 | 179 | recordId 180 | 181 | myVariable_current.Id 182 | 183 | 184 | 185 | 186 | myVariable_waitStartTimeAssignment 187 | 188 | 0 189 | 0 190 | 191 | myVariable_waitStartTimeVariable 192 | Assign 193 | 194 | $Flow.CurrentDateTime 195 | 196 | 197 | 198 | myDecision 199 | 200 | 201 | 202 | 203 | index 204 | 205 | 0.0 206 | 207 | 208 | myDecision 209 | 210 | 50 211 | 0 212 | default 213 | 214 | myRule_1 215 | and 216 | 217 | 218 | inputDataType 219 | 220 | Boolean 221 | 222 | 223 | 224 | leftHandSideType 225 | 226 | Boolean 227 | 228 | 229 | 230 | operatorDataType 231 | 232 | Boolean 233 | 234 | 235 | 236 | rightHandSideType 237 | 238 | Boolean 239 | 240 | 241 | myVariable_current.Is_Member__c 242 | EqualTo 243 | 244 | true 245 | 246 | 247 | 248 | myRule_1_A1 249 | 250 | 251 | 252 | 253 | Example that welcomes new chatter group members 254 | 255 | 256 | originalFormula 257 | 258 | $User.Id 259 | 260 | 261 | formula_2_myRule_1_A1_authorId 262 | String 263 | $User.Id 264 | 265 | Chatter_Bot_Welcome_New_Group_Member-6_Chatter_Bot_Group_Member__c 266 | 267 | 268 | ObjectType 269 | 270 | Chatter_Bot_Group_Member__c 271 | 272 | 273 | 274 | ObjectVariable 275 | 276 | myVariable_current 277 | 278 | 279 | 280 | OldObjectVariable 281 | 282 | myVariable_old 283 | 284 | 285 | 286 | TriggerType 287 | 288 | onCreateOnly 289 | 290 | 291 | Workflow 292 | myVariable_waitStartTimeAssignment 293 | 294 | myVariable_current 295 | SObject 296 | false 297 | true 298 | true 299 | Chatter_Bot_Group_Member__c 300 | 301 | 302 | myVariable_old 303 | SObject 304 | false 305 | true 306 | false 307 | Chatter_Bot_Group_Member__c 308 | 309 | 310 | myVariable_waitStartTimeVariable 311 | DateTime 312 | false 313 | false 314 | false 315 | 316 | $Flow.CurrentDateTime 317 | 318 | 319 | 320 | -------------------------------------------------------------------------------- /src/objects/Chatter_Bot_Feeds_Setting__c.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hierarchy 4 | Preferences for Chatter Bot Feeds 5 | false 6 | 7 | Email_Service_Address_User_ID__c 8 | To avoid consuming daily org-wide email limits, create a Chatter Free user whose email is the Email Service Address of the Apex Email Service that uses ChatterBotPostMessageEmailHandler. The Context User of that service (not the Chatter Free user) must have the system permission "Insert System Field Values for Chatter Feeds" to post as arbitrary users. https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_email.htm 9 | false 10 | To avoid consuming daily org-wide email limits, create a Chatter Free user whose email is the Email Service Address of the Apex Email Service that uses ChatterBotPostMessageEmailHandler. 11 | 12 | 255 13 | false 14 | false 15 | Text 16 | false 17 | 18 | 19 | Protected 20 | 21 | -------------------------------------------------------------------------------- /src/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ChatterBotPostMessageEmailHandler 5 | ChatterBotPostMessageEmailHandlerTest 6 | ChatterBotPostMessageInvocable 7 | ChatterBotPostMessageInvocableTest 8 | ChatterBotPostMessageService 9 | ChatterBotPostMessageServiceTest 10 | ConnectApiHelper 11 | ConnectApiHelperTest 12 | ApexClass 13 | 14 | 15 | Chatter_Bot_Feeds_Setting__c 16 | CustomObject 17 | 18 | 19 | Chatter_Bot_Email_Templates 20 | Chatter_Bot_Email_Templates/Chatter_Bot_Post_Message_Template 21 | EmailTemplate 22 | 23 | 24 | Chatter_Bot_Welcome_New_Group_Member-6 25 | Flow 26 | 27 | 28 | Chatter_Bot_Welcome_New_Group_Member 29 | FlowDefinition 30 | 31 | 32 | Chatter_Bot_Feeds_Admin 33 | PermissionSet 34 | 35 | 38.0 36 | 37 | -------------------------------------------------------------------------------- /src/permissionsets/Chatter_Bot_Feeds_Admin.permissionset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ChatterBotPostMessageEmailHandler 5 | false 6 | 7 | 8 | ChatterBotPostMessageEmailHandlerTest 9 | false 10 | 11 | 12 | ChatterBotPostMessageInvocable 13 | false 14 | 15 | 16 | ChatterBotPostMessageInvocableTest 17 | false 18 | 19 | 20 | ChatterBotPostMessageService 21 | false 22 | 23 | 24 | ChatterBotPostMessageServiceTest 25 | false 26 | 27 | 28 | ConnectApiHelper 29 | false 30 | 31 | 32 | ConnectApiHelperTest 33 | false 34 | 35 | Grants "Insert System Field Values for Chatter Feeds" system permission. 36 | false 37 | 38 | 39 | true 40 | CanInsertFeedSystemFields 41 | 42 | 43 | true 44 | ChatterInternalUser 45 | 46 | 47 | --------------------------------------------------------------------------------