Skip to content

Commit

Permalink
[SET-627] Enable chatter
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
nicolasbock committed May 7, 2024
1 parent 9cfc677 commit e9a493a
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 15 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
1 change: 0 additions & 1 deletion cmd/monitor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ func init() {
}

func main() {

cfg, err := config.NewConfigFromFile(*configs)
if err != nil {
panic(err)
Expand Down
175 changes: 175 additions & 0 deletions cmd/salesforce-test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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 {
getAllCases(sfClient)
}

if *allFeedComments {
getAllFeedComments(sfClient)
}

if *allFeedItems {
getAllFeedItems(sfClient)
}

if len(*caseNumber) > 0 {
caseId := getCase(sfClient)
if *getComments {
getAllCaseComments(caseId, sfClient)
}

if *getChatter {
getAllChatterComments(caseId, sfClient)
}

if len(*newChatter) > 0 {
newChatterComment(sfClient, caseId, *newChatter)
}
}
}

func newChatterComment(sfClient common.SalesforceClient, caseId string, comment string) {
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", comment).
Set("Visibility", visibility).
Create()
}

func getAllChatterComments(caseId string, sfClient common.SalesforceClient) {
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"])
}
}

func getAllCaseComments(caseId string, sfClient common.SalesforceClient) {
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"])
}
}

func getCase(sfClient common.SalesforceClient) string {
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)
}
return fmt.Sprintf("%s", records.Records[0]["Id"])
}

func getAllFeedItems(sfClient common.SalesforceClient) {
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"])
}
}

func getAllFeedComments(sfClient common.SalesforceClient) {
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"])
}
}

func getAllCases(sfClient common.SalesforceClient) {
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"])
}
}
31 changes: 23 additions & 8 deletions pkg/common/salesforce.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
4 changes: 3 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ type SalesForce struct {
Password string `yaml:"password"`
SecurityToken string `yaml:"security-token"`
MaxCommentLength int `yaml:"max-comment-length"`
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
EnableChatter: false,
}
}

Expand Down Expand Up @@ -129,8 +131,8 @@ func NewConfigFromFile(filePaths []string) (*Config, error) {

if err := s.Snuffle(); err != nil {
return nil, err

}

return &config, nil
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ 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")
}
}

func TestNewConfigFromFile(t *testing.T) {
Expand Down
13 changes: 10 additions & 3 deletions pkg/processor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -507,8 +508,14 @@ 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.EnableChatter {
comment = p.SalesforceClient.PostChatter(caseId,
chunkHeader+chunk, subscriber.SFCommentIsPublic)
} else {
comment = p.SalesforceClient.PostComment(caseId,
chunkHeader+chunk, subscriber.SFCommentIsPublic)
}
if comment == nil {
log.Errorf("Failed to post comment to case id: %s", caseId)
continue
Expand Down

0 comments on commit e9a493a

Please sign in to comment.