├── .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 | 
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 | [](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 | 
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 | 
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 | * , , , ,
, ,
,
.
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]*>)|([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]*>)|([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('');
183 | if (isBeginTag) {
184 | // Strip off the < and >.
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 and >.
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,
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 |
13 | Chatter messages support @ mentions by placing a Chatter User, Group, or Record ID between two curly brackets like this {userId}.
14 |
15 |
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 |
18 |
19 | Here's an example where we are @ mentioning your name {{!Chatter_Bot_Group_Member__c.MemberId__c}}
20 |
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> </p> to create a blank paragraph with same effect.
65 |