From 6e17e83661f26819bc2a242cc877a28a56e1f30a Mon Sep 17 00:00:00 2001 From: Nicolas Bock Date: Tue, 7 May 2024 09:00:38 -0600 Subject: [PATCH] [SET-627] Enable chatter This change adds a configuration option to post comments to a chatter object instead of a casecomment object. The feature is disabled by default and has to be explicitly enabled via the configuration file: ```yaml salesforce: enable-chatter: true ``` The change also introduces the `salesforce-test` binary to test Salesforce queries. Closes: SET-627 Signed-off-by: Nicolas Bock --- Makefile | 6 +- cmd/monitor/main.go | 1 - cmd/salesforce-test/main.go | 149 ++++++++++++++++++++++++++++++++++++ pkg/common/salesforce.go | 31 ++++++-- pkg/common/utils.go | 2 +- pkg/config/config.go | 10 +++ pkg/config/config_test.go | 8 ++ pkg/processor/processor.go | 15 +++- 8 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 cmd/salesforce-test/main.go diff --git a/Makefile b/Makefile index 2b15556..838f70b 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ debug-container: . .PHONY: build -build: athena-monitor athena-processor +build: athena-monitor athena-processor salesforce-test .PHONY: athena-monitor athena-monitor: @@ -51,6 +51,10 @@ athena-monitor: athena-processor: go build -v -o $@ -ldflags="-X main.commit=$$(git describe --tags)" cmd/processor/main.go +.PHONY: salesforce-test +salesforce-test: + go build -v -o $@ -ldflags="-X main.commit=$$(git describe --tags)" cmd/salesforce-test/main.go + .PHONY: lint lint: check_modules gofmt diff --git a/cmd/monitor/main.go b/cmd/monitor/main.go index 79cb672..2e75b24 100644 --- a/cmd/monitor/main.go +++ b/cmd/monitor/main.go @@ -29,7 +29,6 @@ func init() { } func main() { - cfg, err := config.NewConfigFromFile(*configs) if err != nil { panic(err) diff --git a/cmd/salesforce-test/main.go b/cmd/salesforce-test/main.go new file mode 100644 index 0000000..5562374 --- /dev/null +++ b/cmd/salesforce-test/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "fmt" + "log" + "strconv" + + "github.com/canonical/athena-core/pkg/common" + "github.com/canonical/athena-core/pkg/config" + "gopkg.in/alecthomas/kingpin.v2" +) + +var configs = common.StringList( + kingpin.Flag("config", "Path to the athena configuration file").Default("/etc/athena/main.yaml").Short('c'), +) + +var allCases = kingpin.Flag("all-cases", "Get all cases").Default("false").Bool() +var allFeedComments = kingpin.Flag("all-feed-comments", "Get all FeedComments").Default("false").Bool() +var allFeedItems = kingpin.Flag("all-feed-items", "Get all FeedItems").Default("false").Bool() +var caseNumber = kingpin.Flag("case-id", "The case ID to query").Default("").String() +var commentVisibility = kingpin.Flag("visibility", "Set the comment visibility {public, private)").Default("private").String() +var getChatter = kingpin.Flag("chatter", "Get all chatter objects of case").Default("false").Bool() +var getComments = kingpin.Flag("comments", "Get all comments of case").Default("false").Bool() +var newChatter = kingpin.Flag("new-chatter", "Add a new chatter comment to the case").Default("").String() + +func main() { + kingpin.HelpFlag.Short('h') + kingpin.Parse() + + switch *commentVisibility { + case "public", "private": + // All good, do nothing. + default: + log.Fatal("Invalid visibility value. Allowed values are 'public' and 'private'.") + } + + cfg, err := config.NewConfigFromFile(*configs) + if err != nil { + panic(err) + } + + sfClient, err := common.NewSalesforceClient(cfg) + if err != nil { + panic(err) + } + + if *allCases { + log.Println("All cases:") + records, err := sfClient.Query("SELECT Id, CaseNumber from Case") + if err != nil { + log.Fatalln("Failed to query for all cases") + } + for _, result := range records.Records { + log.Printf("%s: %s", result["Id"], result["CaseNumber"]) + } + } + + if *allFeedComments { + log.Println("All FeedComments:") + records, err := sfClient.Query("SELECT Id from FeedComment") + if err != nil { + log.Fatalln("Failed to query for all FeedComments") + } + for _, result := range records.Records { + log.Printf("%s", result["Id"]) + } + } + + if *allFeedItems { + log.Println("All FeedItems:") + records, err := sfClient.Query("SELECT Id from FeedItem") + if err != nil { + log.Fatalln("Failed to query for all FeedItems") + } + for _, result := range records.Records { + log.Printf("%s", result["Id"]) + } + } + + if len(*caseNumber) > 0 { + // Query Salesforce for the case. + caseNumberAsInt, err := strconv.ParseInt(*caseNumber, 10, 64) + if err != nil { + log.Fatalf("Failed to parse the case number %s", *caseNumber) + } + caseNumberFormatted := fmt.Sprintf("%08d", caseNumberAsInt) + + log.Printf("Searching for case %s", caseNumberFormatted) + query := fmt.Sprintf("SELECT Id, CaseNumber FROM Case WHERE CaseNumber = '%s'", caseNumberFormatted) + records, err := sfClient.Query(query) + if err != nil { + log.Fatalf("Failed to query Salesforce: %v", err) + } + if len(records.Records) > 0 { + log.Printf("%s: %s", records.Records[0]["Id"], records.Records[0]["CaseNumber"]) + } else { + log.Fatalf("Case with ID %s does not exist.\n", *caseNumber) + } + caseId := records.Records[0]["Id"] + + if *getComments { + log.Print("Getting case comments") + query = fmt.Sprintf("SELECT Id, CommentBody FROM CaseComment WHERE ParentId = '%s'", caseId) + records, err = sfClient.Query(query) + if err != nil { + log.Fatalf("Failed to get case comments: %v", err) + } + if len(records.Records) == 0 { + log.Fatal("Could not find any case comments") + } + for _, comment := range records.Records { + log.Printf("%s", comment["CommentBody"]) + } + } + + if *getChatter { + log.Print("Getting case chatter comments") + query = fmt.Sprintf("SELECT Id, Body FROM FeedItem WHERE ParentID = '%s'", caseId) + records, err = sfClient.Query(query) + if err != nil { + log.Fatalf("Failed to get chatter comments: %v", err) + } + if len(records.Records) == 0 { + log.Fatal("Could not find any chatter comments") + } + for _, comment := range records.Records { + log.Printf("%s: %s", comment["Id"], comment["Body"]) + } + } + + if len(*newChatter) > 0 { + log.Print("Added new chatter comment") + visibility := "" + switch *commentVisibility { + case "public": + visibility = "AllUsers" + case "private": + visibility = "InternalUsers" + default: + log.Fatal("Unknown visibility") + } + sfClient.SObject("FeedItem"). + Set("ParentId", caseId). + Set("Body", *newChatter). + Set("Visibility", visibility). + Create() + } + } +} diff --git a/pkg/common/salesforce.go b/pkg/common/salesforce.go index b8413cd..ec4cc9c 100644 --- a/pkg/common/salesforce.go +++ b/pkg/common/salesforce.go @@ -20,8 +20,11 @@ func (e ErrNoCaseFound) Error() string { var ErrAuthentication = simpleforce.ErrAuthentication type SalesforceClient interface { + Query(query string) (*simpleforce.QueryResult, error) + SObject(objectName ...string) *simpleforce.SObject GetCaseByNumber(number string) (*Case, error) PostComment(caseId, body string, isPublic bool) *simpleforce.SObject + PostChatter(caseId, body string, isPublic bool) *simpleforce.SObject } type BaseSalesforceClient struct { @@ -40,14 +43,6 @@ type Case struct { Id, CaseNumber, AccountId, Customer string } -func (sf *BaseSalesforceClient) PostComment(caseId, body string, isPublic bool) *simpleforce.SObject { - return sf.SObject("CaseComment"). - Set("ParentId", caseId). - Set("CommentBody", html.UnescapeString(body)). - Set("IsPublished", isPublic). - Create() -} - func (sf *BaseSalesforceClient) GetCaseByNumber(number string) (*Case, error) { q := "SELECT Id,CaseNumber,AccountId FROM Case WHERE CaseNumber LIKE '%" + number + "%'" result, err := sf.Query(q) @@ -72,6 +67,26 @@ func (sf *BaseSalesforceClient) GetCaseByNumber(number string) (*Case, error) { return nil, ErrNoCaseFound{number} } +func (sf *BaseSalesforceClient) PostComment(caseId, body string, isPublic bool) *simpleforce.SObject { + return sf.SObject("CaseComment"). + Set("ParentId", caseId). + Set("CommentBody", html.UnescapeString(body)). + Set("IsPublished", isPublic). + Create() +} + +func (sf *BaseSalesforceClient) PostChatter(caseId, body string, isPublic bool) *simpleforce.SObject { + visibility := "InternalUsers" + if isPublic { + visibility = "AllUsers" + } + return sf.SObject("FeedItem"). + Set("ParentId", caseId). + Set("Body", body). + Set("Visibility", visibility). + Create() +} + func GetCaseNumberFromFilename(filename string) (string, error) { regex, err := regexp.Compile(`(\d{6,})`) if err != nil { diff --git a/pkg/common/utils.go b/pkg/common/utils.go index e68b290..d08e654 100644 --- a/pkg/common/utils.go +++ b/pkg/common/utils.go @@ -7,7 +7,7 @@ import ( ) func RunOnInterval(ctx context.Context, lock *sync.Mutex, d time.Duration, f func(ctx *context.Context, interval time.Duration)) { - ticker := time.Tick(d) // nolint:staticcheck + ticker := time.Tick(d) for { select { case <-ctx.Done(): diff --git a/pkg/config/config.go b/pkg/config/config.go index 5a1e889..7e09c11 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( "github.com/makyo/snuffler" + log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) @@ -76,11 +77,15 @@ type SalesForce struct { Password string `yaml:"password"` SecurityToken string `yaml:"security-token"` MaxCommentLength int `yaml:"max-comment-length"` + EnableComments bool `yaml:"enable-comments"` + EnableChatter bool `yaml:"enable-chatter"` } func NewSalesForce() SalesForce { return SalesForce{ MaxCommentLength: 4000 - 1000, // A very conservative buffer of max length per Salesforce comment (4000) without header text for comments + EnableComments: true, + EnableChatter: false, } } @@ -129,8 +134,13 @@ func NewConfigFromFile(filePaths []string) (*Config, error) { if err := s.Snuffle(); err != nil { return nil, err + } + if config.Salesforce.EnableComments && config.Salesforce.EnableChatter { + log.Warnln("Will post to Chatter in favor of regular case comments") + config.Salesforce.EnableComments = false } + return &config, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 19154cb..2db1772 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -88,6 +88,14 @@ func TestNewSalesforce(t *testing.T) { if salesforce.MaxCommentLength != 3000 { t.Errorf("Expected MaxCommentLength to be 3000, got '%d'", salesforce.MaxCommentLength) } + + if salesforce.EnableChatter { + t.Errorf("Expected EnableChatter to be false, got true") + } + + if !salesforce.EnableComments { + t.Errorf("Expected EnableComments to be true, got false") + } } func TestNewConfigFromFile(t *testing.T) { diff --git a/pkg/processor/processor.go b/pkg/processor/processor.go index 80ba643..d834e18 100644 --- a/pkg/processor/processor.go +++ b/pkg/processor/processor.go @@ -17,6 +17,7 @@ import ( "github.com/flosch/pongo2/v4" "github.com/lileio/pubsub/v2" "github.com/lileio/pubsub/v2/middleware/defaults" + "github.com/simpleforce/simpleforce" log "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -455,7 +456,7 @@ func (p *Processor) BatchSalesforceComments(ctx *context.Context, interval time. return } - log.Infof("Found %d reports to be sent to salesforce", len(reports)) + log.Infof("Found %d reports to be sent to Salesforce", len(reports)) for _, report := range reports { if reportMap[report.Subscriber] == nil { reportMap[report.Subscriber] = make(map[string]map[string][]db.Report) @@ -507,8 +508,16 @@ func (p *Processor) BatchSalesforceComments(ctx *context.Context, interval time. if len(commentChunks) > 1 { chunkHeader = fmt.Sprintf("Split comment %d of %d\n\n", i+1, len(commentChunks)) } - comment := p.SalesforceClient.PostComment(caseId, - chunkHeader+chunk, subscriber.SFCommentIsPublic) + var comment *simpleforce.SObject + if p.Config.Salesforce.EnableComments { + comment = p.SalesforceClient.PostComment(caseId, + chunkHeader+chunk, subscriber.SFCommentIsPublic) + } else if p.Config.Salesforce.EnableChatter { + comment = p.SalesforceClient.PostChatter(caseId, + chunkHeader+chunk, subscriber.SFCommentIsPublic) + } else { + log.Warnln("Neither comments nor chatter is enabled") + } if comment == nil { log.Errorf("Failed to post comment to case id: %s", caseId) continue