TP-44162 | Google Meet integration to create calendar invite with meeting link creation on incident creation (#277)

* TP-44158 | Adding service to get transcript files from Google Drive (#234)

Adding service to get transcript files from Google Drive

* TP-45120 (#275)

TP-45120 | merging Google auth implementation and calendar event fix

* TP-44162 | added service implementation for calendar actions

* TP-44162 | Updated label in slack message

* TP-44162 | Fixed build failures

* TP-44162 | Updated sql migration file name

* TP-44162 | added unit tests for google calendar service

* TP-48200 | updated response messages in link and unlink jira apis (#278)

* TP-44162 | resolved review comments

* TP-44158 | Adding service to get transcript files from Google Drive (#234)

Adding service to get transcript files from Google Drive

* TP-45120 (#275)

TP-45120 | merging Google auth implementation and calendar event fix

* TP-44162 | added service implementation for calendar actions

* TP-44162 | Updated label in slack message

* TP-44162 | Fixed build failures

* TP-44162 | Updated sql migration file name

* TP-44162 | added unit tests for google calendar service

* TP-44162 | resolved review comments

* TP-44162 | updated few naming conventions

* TP-44162 | Adding timeouts to google drive api calls and related UTs

* TP-44162 | Adding drive api timeout to viper for unit test

---------

Co-authored-by: Sriram Bhargav <sriram.bhargav@navi.com>
Co-authored-by: Shashank Shekhar <shashank.shekhar@navi.com>
This commit is contained in:
Ajay Devarakonda
2023-11-09 16:25:20 +05:30
committed by GitHub
parent f075b0df8f
commit 1125f573b2
23 changed files with 825 additions and 131 deletions

2
.gitignore vendored
View File

@@ -17,6 +17,8 @@
/.idea /.idea
.idea .idea
/out /out
/mocks
*_mock.go
go.sum go.sum

View File

@@ -1,15 +1,11 @@
.PHONY: build .PHONY: docker-run
build: docker-run: docker-build
go mod tidy && CGO_ENABLED=0 go build -ldflags="-s -w" -o houston cmd/main.go docker run houston
.PHONY: docker-build .PHONY: docker-build
docker-build: docker-build:
docker build -t houston -f Dockerfile.houston . docker build -t houston -f Dockerfile.houston .
.PHONY: docker-run
docker-run: docker-build
docker run houston
.PHONY: migration-up .PHONY: migration-up
migration-up: migration-up:
migrate -path db/migration/ -database "${POSTGRES_DSN}?sslmode=disable" -verbose up migrate -path db/migration/ -database "${POSTGRES_DSN}?sslmode=disable" -verbose up
@@ -17,3 +13,23 @@ migration-up:
.PHONY: migration-down .PHONY: migration-down
migration-down: migration-down:
migrate -path db/migration/ -database "${POSTGRES_DSN}?sslmode=disable" -verbose down migrate -path db/migration/ -database "${POSTGRES_DSN}?sslmode=disable" -verbose down
.PHONY: build
build: test
go mod tidy && CGO_ENABLED=0 go build -ldflags="-s -w" -o houston cmd/main.go
.PHONY: test
test: generatemocks
go test -v -count=1 $(CURDIR)/service/...
@rm -rf $(CURDIR)/mocks
# Keep all mock file generation below this line
.PHONY: generatemocks
generatemocks:
@go install github.com/gojuno/minimock/v3/cmd/minimock@v3.1.3
@go mod tidy
@rm -rf $(CURDIR)/mocks
@echo "Generating mocks..."
@mkdir "mocks"
cd $(CURDIR)/pkg/google/googleDrive && minimock -i GoogleDriveActions -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/pkg/conference && minimock -i ICalendarActions -s _mock.go -o $(CURDIR)/mocks

View File

@@ -45,3 +45,11 @@ const (
AlreadyArchivedError = "already_archived" AlreadyArchivedError = "already_archived"
NotInChannelError = "not_in_channel" NotInChannelError = "not_in_channel"
) )
const (
GoogleDriveFileMimeType = "application/vnd.google-apps.folder"
)
const (
ConferenceMessage = "To discuss, use this *<%s|Meet link>*"
)

View File

@@ -9,6 +9,7 @@ import (
"houston/model/incident" "houston/model/incident"
"houston/model/severity" "houston/model/severity"
"houston/model/team" "houston/model/team"
"houston/pkg/conference"
"houston/pkg/slackbot" "houston/pkg/slackbot"
"strings" "strings"
"time" "time"
@@ -90,3 +91,12 @@ func getOncallOrResponderHandle(teamEntity *team.TeamEntity, severityEntity *sev
func isPseIncident(teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity) bool { func isPseIncident(teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity) bool {
return len(strings.TrimSpace(teamEntity.PseOncallHandle)) > 0 && (severityEntity.Name == "Sev-2" || severityEntity.Name == "Sev-3") return len(strings.TrimSpace(teamEntity.PseOncallHandle)) > 0 && (severityEntity.Name == "Sev-2" || severityEntity.Name == "Sev-3")
} }
func UpdateIncidentWithConferenceDetails(incidentEntity *incident.IncidentEntity, event conference.EventData, repo *incident.Repository) {
incidentEntity.ConferenceLink = event.ConferenceLink
incidentEntity.ConferenceId = event.Id
err := repo.UpdateIncident(incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("Unable to update incident %s with conference details due to error: %s", incidentEntity.IncidentName, err.Error()))
}
}

View File

@@ -58,9 +58,12 @@ s3.bucket.name=S3_BUCKET_NAME
s3.access.key=S3_ACCESS_KEY s3.access.key=S3_ACCESS_KEY
s3.secret.key=S3_SECRET_KEY s3.secret.key=S3_SECRET_KEY
#gmeet #conference
gmeet.enable=ENABLE_GMEET conference.enable=ENABLE_CONFERENCE
gmeet.config.file.path=GMEET_CONFIG_FILE_PATH conference.type=CONFERENCE_TYPE
google.auth.key.content=GOOGLE_AUTH_KEY_CONTENT
google.auth.email=GOOGLE_AUTH_EMAIL
google.api.timeout=GOOGLE_API_TIMEOUT
#gocd #gocd
gocd.sa.baseurl=GOCD_SA_BASEURL gocd.sa.baseurl=GOCD_SA_BASEURL

View File

@@ -0,0 +1,2 @@
alter table incident add column if not exists conference_id varchar(64);
alter table incident add column if not exists conference_link varchar(64);

9
go.mod
View File

@@ -10,6 +10,7 @@ require (
github.com/chromedp/chromedp v0.9.1 github.com/chromedp/chromedp v0.9.1
github.com/gin-contrib/zap v0.1.0 github.com/gin-contrib/zap v0.1.0
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/gojuno/minimock/v3 v3.1.3
github.com/google/uuid v1.4.0 github.com/google/uuid v1.4.0
github.com/jackc/pgx/v5 v5.3.1 github.com/jackc/pgx/v5 v5.3.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
@@ -17,9 +18,11 @@ require (
github.com/slack-go/slack v0.12.1 github.com/slack-go/slack v0.12.1
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.3
github.com/thoas/go-funk v0.9.3 github.com/thoas/go-funk v0.9.3
go.uber.org/zap v1.24.0 go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/oauth2 v0.13.0
gorm.io/datatypes v1.2.0 gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.2 gorm.io/driver/postgres v1.5.2
gorm.io/gorm v1.25.2 gorm.io/gorm v1.25.2
@@ -47,6 +50,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
@@ -60,13 +64,12 @@ require (
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.59.0 // indirect google.golang.org/grpc v1.59.0 // indirect
gorm.io/driver/mysql v1.4.7 // indirect gorm.io/driver/mysql v1.4.7 // indirect
@@ -91,7 +94,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

View File

@@ -0,0 +1,28 @@
package clients
import (
"context"
"fmt"
"github.com/spf13/viper"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/drive/v2"
"houston/logger"
)
func getGoogleAuthJWTConfig() *jwt.Config {
content := viper.GetString("GOOGLE_AUTH_KEY_CONTENT")
//If modifying these scopes, update the scopes in service account and regenerate the keys and update the deployment portal.
config, err := google.JWTConfigFromJSON([]byte(content), calendar.CalendarScope, drive.DriveScope)
if err != nil {
logger.Error(fmt.Sprintf("Unable to create google JWT config due to error: %s", err.Error()))
}
config.Subject = viper.GetString("GOOGLE_AUTH_EMAIL")
return config
}
func GetGoogleTokenSource() oauth2.TokenSource {
return getGoogleAuthJWTConfig().TokenSource(context.Background())
}

View File

@@ -130,7 +130,7 @@ func (dip *DuplicateIncidentAction) DuplicateIncidentProcess(callback slack.Inte
} }
} }
}() }()
//ToDo() Delete Conference event if exists and if incident is duplicated
} else { } else {
msgOption := slack.MsgOptionText(fmt.Sprintf("`Submitted incident id: %s is not a valid open incident. Check and resubmit`", incidentRca), false) msgOption := slack.MsgOptionText(fmt.Sprintf("`Submitted incident id: %s is not a valid open incident. Check and resubmit`", incidentRca), false)
_, errMessage := dip.client.PostEphemeral(channelId, user.ID, msgOption) _, errMessage := dip.client.PostEphemeral(channelId, user.ID, msgOption)

View File

@@ -116,6 +116,8 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact
} }
msgUpdate := NewIncidentChannelMessageUpdateAction(irp.client, irp.incidentService, irp.teamRepository, irp.severityRepository) msgUpdate := NewIncidentChannelMessageUpdateAction(irp.client, irp.incidentService, irp.teamRepository, irp.severityRepository)
msgUpdate.ProcessAction(incidentEntity.SlackChannel) msgUpdate.ProcessAction(incidentEntity.SlackChannel)
//ToDo() Delete Conference event if exists and if incident is resolved
go func() { go func() {
if incidentEntity.SeverityId != incident.Sev0Id && incidentEntity.SeverityId != incident.Sev1Id { if incidentEntity.SeverityId != incident.Sev0Id && incidentEntity.SeverityId != incident.Sev1Id {
postErr := util.PostArchivingTimeToIncidentChannel(channelId, incident.Resolved, irp.client) postErr := util.PostArchivingTimeToIncidentChannel(channelId, incident.Resolved, irp.client)

View File

@@ -1,7 +1,6 @@
package action package action
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"gorm.io/gorm" "gorm.io/gorm"
@@ -11,19 +10,18 @@ import (
"houston/model/incident" "houston/model/incident"
"houston/model/severity" "houston/model/severity"
"houston/model/team" "houston/model/team"
conference2 "houston/pkg/conference"
"houston/pkg/slackbot" "houston/pkg/slackbot"
"houston/service/conference"
incidentService "houston/service/incident" incidentService "houston/service/incident"
request "houston/service/request" request "houston/service/request"
"strings" "strings"
"time" "time"
"github.com/google/uuid"
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode" "github.com/slack-go/slack/socketmode"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
incidentHelper "houston/common/util" incidentHelper "houston/common/util"
) )
@@ -36,14 +34,7 @@ type CreateIncidentAction struct {
db *gorm.DB db *gorm.DB
} }
func NewCreateIncidentProcessor( func NewCreateIncidentProcessor(client *socketmode.Client, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository, slackbotClient *slackbot.Client, db *gorm.DB) *CreateIncidentAction {
client *socketmode.Client,
incidentService *incident.Repository,
teamService *team.Repository,
severityService *severity.Repository,
slackbotClient *slackbot.Client,
db *gorm.DB,
) *CreateIncidentAction {
return &CreateIncidentAction{ return &CreateIncidentAction{
client: client, client: client,
incidentRepository: incidentService, incidentRepository: incidentService,
@@ -123,16 +114,29 @@ func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessing(callback s
return return
} }
} }
if viper.GetBool("ENABLE_GMEET") { if viper.GetBool("ENABLE_CONFERENCE") {
gmeet, err := createGmeetLink(*channelID) calendarActions := conference2.GetCalendarActions()
calendarService := conference.NewCalendarService(calendarActions)
calendarEvent, err := calendarService.CreateEvent(incidentEntity.IncidentName)
if err != nil { if err != nil {
logger.Error("[CIP] error while creating gmeet", zap.Error(err)) logger.Error(fmt.Sprintf("Unable to create conference event due to error : %s", err.Error()))
} else { } else {
msgOption := slack.MsgOptionText(fmt.Sprintf("gmeet: ", gmeet), false) util.UpdateIncidentWithConferenceDetails(incidentEntity, calendarEvent, isp.incidentRepository)
isp.client.PostMessage(*channelID, msgOption) msgUpdate := NewIncidentChannelMessageUpdateAction(isp.client, isp.incidentRepository, isp.teamRepository, isp.severityRepository)
msgUpdate.ProcessAction(incidentEntity.SlackChannel)
bookmarkParam := slack.AddBookmarkParameters{Link: calendarEvent.ConferenceLink, Title: calendarService.GetConferenceTitle()}
_, err := isp.client.AddBookmark(*channelID, bookmarkParam)
if err != nil {
logger.Error(fmt.Sprintf("Unable to add conference link as bookmark for channel %s due to error: %s", incidentEntity.SlackChannel, err.Error()))
}
msgOption := slack.MsgOptionText(fmt.Sprintf(util.ConferenceMessage, calendarEvent.ConferenceLink), false)
_, _, err = isp.client.PostMessage(*channelID, msgOption)
if err != nil {
logger.Error(fmt.Sprintf("Unable to post message to channel %s due to error: %s", incidentEntity.SlackChannel, err.Error()))
}
} }
} }
}() }()
// Acknowledge the interaction callback // Acknowledge the interaction callback
@@ -179,47 +183,6 @@ func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessingV2(
isp.client.Ack(*request, payload) isp.client.Ack(*request, payload)
} }
func createGmeetLink(channelName string) (string, error) {
calclient, err := calendar.NewService(context.Background(), option.WithCredentialsFile(viper.GetString("GMEET_CONFIG_FILE_PATH")))
if err != nil {
logger.Error("Unable to read client secret file: ", zap.Error(err))
return "", err
}
t0 := time.Now().Format(time.RFC3339)
t1 := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
event := &calendar.Event{
Summary: channelName,
Description: "Incident",
Start: &calendar.EventDateTime{
DateTime: t0,
},
End: &calendar.EventDateTime{
DateTime: t1,
},
ConferenceData: &calendar.ConferenceData{
CreateRequest: &calendar.CreateConferenceRequest{
RequestId: uuid.NewString(),
ConferenceSolutionKey: &calendar.ConferenceSolutionKey{
Type: "hangoutsMeet",
},
Status: &calendar.ConferenceRequestStatus{
StatusCode: "success",
},
},
},
}
calendarID := "primary" //use "primary"
event, err = calclient.Events.Insert(calendarID, event).ConferenceDataVersion(1).Do()
if err != nil {
logger.Error("Unable to create event. %v\n", zap.Error(err))
return "", err
}
calclient.Events.Delete(calendarID, event.Id).Do()
return event.HangoutLink, nil
}
func (isp *CreateIncidentAction) createSlackChannel(incidentEntity *incident.IncidentEntity) (*string, error) { func (isp *CreateIncidentAction) createSlackChannel(incidentEntity *incident.IncidentEntity) (*string, error) {
var channelName string var channelName string
if viper.GetString("env") != "prod" { if viper.GetString("env") != "prod" {

View File

@@ -17,7 +17,7 @@ func IncidentSummarySection(incident *incident.IncidentEntity, team *team.TeamEn
buildDescriptionBlock(incident.Description), buildDescriptionBlock(incident.Description),
buildTypeAndChannelSectionBlock(incident, team.Name), buildTypeAndChannelSectionBlock(incident, team.Name),
buildSeverityAndTicketSectionBlock(severity.Name), buildSeverityAndTicketSectionBlock(severity.Name),
buildStatusAndMeetLinkSectionBlock(incidentStatus.Name), buildStatusAndConferenceLinkSectionBlock(incidentStatus.Name, incident.ConferenceLink),
buildCreatedByAndCreatedAtSectionBlock(incident), buildCreatedByAndCreatedAtSectionBlock(incident),
}, },
} }
@@ -62,10 +62,14 @@ func buildSeverityAndTicketSectionBlock(severityName string) *slack.SectionBlock
return block return block
} }
func buildStatusAndMeetLinkSectionBlock(incidentStatus string) *slack.SectionBlock { func buildStatusAndConferenceLinkSectionBlock(incidentStatus string, conferenceLink string) *slack.SectionBlock {
var conferenceLinkLabel = "Integration Disabled"
if conferenceLink != "" {
conferenceLinkLabel = conferenceLink
}
fields := []*slack.TextBlockObject{ fields := []*slack.TextBlockObject{
slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Status*\n%s", incidentStatus), false, false), slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Status*\n%s", incidentStatus), false, false),
slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Meeting*\n%s", "Integration Disabled"), false, false), slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Meeting*\n%s", conferenceLinkLabel), false, false),
} }
block := slack.NewSectionBlock(nil, fields, nil) block := slack.NewSectionBlock(nil, fields, nil)

View File

@@ -63,6 +63,8 @@ type IncidentEntity struct {
UpdatedBy string `gorm:"column:updated_by"` UpdatedBy string `gorm:"column:updated_by"`
MetaData JSON `gorm:"column:meta_data"` MetaData JSON `gorm:"column:meta_data"`
RCA string `gorm:"column:rca_text"` RCA string `gorm:"column:rca_text"`
ConferenceId string `gorm:"column:conference_id"`
ConferenceLink string `gorm:"column:conference_link"`
} }
func (IncidentEntity) TableName() string { func (IncidentEntity) TableName() string {

View File

@@ -0,0 +1,42 @@
package conference
import (
"context"
"fmt"
"github.com/spf13/viper"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
"houston/internal/clients"
"houston/logger"
)
type EventData struct {
EventName string
Id string
ConferenceLink string
}
type ICalendarActions interface {
GetConferenceTitle() string
CreateEvent(eventData EventData) (EventData, error)
DeleteEvent(eventId string) error
GetEvent(eventId string) (EventData, error)
}
func GetCalendarActions() ICalendarActions {
switch viper.GetString("CONFERENCE_TYPE") {
case "GMEET":
actions, _ := newGoogleCalendarActions()
return actions
}
return nil
}
func newGoogleCalendarActions() (ICalendarActions, error) {
service, err := calendar.NewService(context.Background(), option.WithTokenSource(clients.GetGoogleTokenSource()))
if err != nil {
logger.Error(fmt.Sprintf("Unable to retrieve googleCalendar client due to error : %s", err))
return nil, err
}
return &GoogleCalendarActionsImpl{CalendarService: service}, nil
}

View File

@@ -0,0 +1,60 @@
package conference
import (
"github.com/google/uuid"
"google.golang.org/api/calendar/v3"
"time"
)
const GoogleMeetTitle = "Google Meet"
const calendarId = "primary"
type GoogleCalendarActionsImpl struct {
CalendarService *calendar.Service
}
func (calendarActions *GoogleCalendarActionsImpl) GetConferenceTitle() string {
return GoogleMeetTitle
}
func (calendarActions *GoogleCalendarActionsImpl) CreateEvent(eventData EventData) (EventData, error) {
startTime := calendar.EventDateTime{Date: time.Now().Format("2006-01-02"), TimeZone: "IST"}
event := &calendar.Event{
Summary: eventData.EventName,
Description: "Google Calendar Meeting Invite for Incident " + eventData.EventName,
Start: &startTime,
//We are using end date as start date because event is a daily recurring event
End: &startTime,
ConferenceData: &calendar.ConferenceData{
CreateRequest: &calendar.CreateConferenceRequest{
RequestId: uuid.NewString(),
ConferenceSolutionKey: &calendar.ConferenceSolutionKey{
Type: "hangoutsMeet",
},
Status: &calendar.ConferenceRequestStatus{
StatusCode: "success",
},
},
},
}
event.Recurrence = []string{"RRULE:FREQ=DAILY"}
event, err := calendarActions.CalendarService.Events.Insert(calendarId, event).ConferenceDataVersion(1).Do()
if err == nil {
eventData.Id = event.Id
eventData.ConferenceLink = event.HangoutLink
}
return eventData, err
}
func (calendarActions *GoogleCalendarActionsImpl) DeleteEvent(eventId string) error {
return calendarActions.CalendarService.Events.Delete(calendarId, eventId).Do()
}
func (calendarActions *GoogleCalendarActionsImpl) GetEvent(eventId string) (EventData, error) {
event, err := calendarActions.CalendarService.Events.Get(calendarId, eventId).Do()
eventData := EventData{Id: eventId}
if err == nil {
eventData.ConferenceLink = event.HangoutLink
}
return eventData, err
}

View File

@@ -0,0 +1,54 @@
package googleDrive
import (
"context"
"google.golang.org/api/drive/v3"
"houston/common/util"
"time"
)
type GoogleDriveActionsImpl struct {
filesService *drive.FilesService
}
func (driveActions *GoogleDriveActionsImpl) SearchInDrive(timeout time.Duration, query string) (*drive.FileList, error) {
driveContext, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
fileList, err := driveActions.filesService.List().Q(query).Context(driveContext).Do()
if err != nil {
return nil, err
}
return fileList, nil
}
func (driveActions *GoogleDriveActionsImpl) CreateDirectory(timeout time.Duration, directoryName string) (*drive.File, error) {
driveContext, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
directory := &drive.File{MimeType: util.GoogleDriveFileMimeType, Name: directoryName}
file, err := driveActions.filesService.Create(directory).Context(driveContext).Fields("*").Do()
if err != nil {
return nil, err
}
return file, nil
}
func (driveActions *GoogleDriveActionsImpl) DeleteFile(timeout time.Duration, fileId string) error {
driveContext, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
err := driveActions.filesService.Delete(fileId).Context(driveContext).Do()
if err != nil {
return err
}
return nil
}
func (driveActions *GoogleDriveActionsImpl) CopyFile(timeout time.Duration, fileId string, directoryId string) (*drive.File, error) {
driveContext, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
copyFile := &drive.File{Parents: []string{directoryId}}
file, err := driveActions.filesService.Copy(fileId, copyFile).Fields("*").Context(driveContext).Do()
if err != nil {
return nil, err
}
return file, nil
}

View File

@@ -0,0 +1,27 @@
package googleDrive
import (
"context"
"go.uber.org/zap"
"google.golang.org/api/drive/v3"
"google.golang.org/api/option"
"houston/internal/clients"
"houston/logger"
"time"
)
type GoogleDriveActions interface {
SearchInDrive(timeout time.Duration, query string) (*drive.FileList, error)
CreateDirectory(timeout time.Duration, directoryName string) (*drive.File, error)
DeleteFile(timeout time.Duration, fileId string) error
CopyFile(timeout time.Duration, fileId string, directoryId string) (*drive.File, error)
}
func NewGoogleDriveActions() (*GoogleDriveActionsImpl, error) {
driveService, err := drive.NewService(context.Background(), option.WithTokenSource(clients.GetGoogleTokenSource()))
if err != nil {
logger.Error("Unable to retrieve Drive client", zap.Error(err))
return nil, err
}
return &GoogleDriveActionsImpl{filesService: driveService.Files}, nil
}

View File

@@ -0,0 +1,50 @@
package conference
import (
"fmt"
"houston/logger"
"houston/pkg/conference"
)
type CalendarService struct {
calendarActions conference.ICalendarActions
}
func NewCalendarService(calendarActions conference.ICalendarActions) *CalendarService {
return &CalendarService{calendarActions: calendarActions}
}
func (calendarService *CalendarService) CreateEvent(eventName string) (conference.EventData, error) {
eventData := conference.EventData{
EventName: eventName,
}
eventData, err := calendarService.calendarActions.CreateEvent(eventData)
if err != nil {
logger.Error(fmt.Sprintf("Unable to create conference event due to error: %s", err))
}
return eventData, err
}
func (calendarService *CalendarService) DeleteEvent(eventId string) error {
err := calendarService.calendarActions.DeleteEvent(eventId)
if err != nil {
logger.Error(fmt.Sprintf("Unable to delete conference event due to error: %s", err))
} else {
logger.Info("Successfully deleted conference event")
}
return err
}
func (calendarService *CalendarService) GetEvent(eventId string) (conference.EventData, error) {
event, err := calendarService.calendarActions.GetEvent(eventId)
if err != nil {
logger.Error(fmt.Sprintf("Unable to get conference event due to error: %s", err))
} else {
logger.Info("Successfully retrieved conference event details")
}
return event, err
}
func (calendarService *CalendarService) GetConferenceTitle() string {
return calendarService.calendarActions.GetConferenceTitle()
}

View File

@@ -0,0 +1,106 @@
package conference
import (
"errors"
"github.com/gojuno/minimock/v3"
"github.com/magiconair/properties/assert"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
"houston/logger"
"houston/mocks"
"houston/pkg/conference"
"testing"
)
type CalendarServiceSuite struct {
suite.Suite
}
func (suite *CalendarServiceSuite) SetupSuite() {
logger.InitLogger()
viper.Set("CONFERENCE_TYPE", "GMEET")
}
const (
testLabel = "testLabel"
createEventFailedError = "create event failed"
deleteEventFailedError = "delete event failed"
getEventFailedError = "get event failed"
updateEventFailedError = "update event failed"
eventIdLabel = "eventId"
)
var eventData = conference.EventData{
EventName: testLabel,
}
func (suite *CalendarServiceSuite) Test_CreateEventFailed() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
calendarActions := mocks.NewICalendarActionsMock(controller)
calendarActions.CreateEventMock.When(
eventData).Then(conference.EventData{}, errors.New(createEventFailedError))
calendarService := NewCalendarService(calendarActions)
_, err := calendarService.CreateEvent(testLabel)
assert.Equal(suite.T(), err.Error(), createEventFailedError)
}
func (suite *CalendarServiceSuite) Test_CreateEventSuccess() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
calendarActions := mocks.NewICalendarActionsMock(controller)
calendarActions.CreateEventMock.When(
eventData).Then(conference.EventData{Id: testLabel, ConferenceLink: testLabel}, nil)
calendarService := NewCalendarService(calendarActions)
event, _ := calendarService.CreateEvent(testLabel)
assert.Equal(suite.T(), event.Id, testLabel)
assert.Equal(suite.T(), event.ConferenceLink, testLabel)
}
func (suite *CalendarServiceSuite) Test_Delete_EventFailed() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
calendarActions := mocks.NewICalendarActionsMock(controller)
calendarActions.DeleteEventMock.When(
eventIdLabel).Then(errors.New(deleteEventFailedError))
calendarService := NewCalendarService(calendarActions)
err := calendarService.DeleteEvent(eventIdLabel)
assert.Equal(suite.T(), err.Error(), deleteEventFailedError)
}
func (suite *CalendarServiceSuite) Test_Delete_Event_Success() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
calendarActions := mocks.NewICalendarActionsMock(controller)
calendarActions.DeleteEventMock.When(
eventIdLabel).Then(nil)
calendarService := NewCalendarService(calendarActions)
err := calendarService.DeleteEvent(eventIdLabel)
assert.Equal(suite.T(), err, nil)
}
func (suite *CalendarServiceSuite) Test_Get_Event_Success() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
calendarActions := mocks.NewICalendarActionsMock(controller)
calendarActions.GetEventMock.When(
eventIdLabel).Then(conference.EventData{Id: eventIdLabel}, nil)
calendarService := NewCalendarService(calendarActions)
event, _ := calendarService.GetEvent(eventIdLabel)
assert.Equal(suite.T(), event.Id, eventIdLabel)
}
func (suite *CalendarServiceSuite) Test_Get_Event_Failed() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
calendarActions := mocks.NewICalendarActionsMock(controller)
calendarActions.GetEventMock.When(
eventIdLabel).Then(conference.EventData{}, errors.New(getEventFailedError))
calendarService := NewCalendarService(calendarActions)
_, err := calendarService.GetEvent(eventIdLabel)
assert.Equal(suite.T(), err.Error(), getEventFailedError)
}
func TestCalendarService(t *testing.T) {
suite.Run(t, new(CalendarServiceSuite))
}

View File

@@ -0,0 +1,112 @@
package google
import (
"fmt"
"github.com/spf13/viper"
"google.golang.org/api/drive/v3"
"houston/common/util"
houstonLogger "houston/logger"
"houston/pkg/google/googleDrive"
"sort"
"time"
)
type Service struct {
driveActions googleDrive.GoogleDriveActions
driveActionTimeOut time.Duration
}
func NewDriveService(driveActions googleDrive.GoogleDriveActions) *Service {
return &Service{driveActions: driveActions, driveActionTimeOut: viper.GetDuration("google.api.timeout")}
}
func (driveService *Service) createDirectory(directoryName string) (*drive.File, error) {
query := fmt.Sprintf("name = '%s' and mimeType = '%v'", directoryName, util.GoogleDriveFileMimeType)
directoryList, err := driveService.driveActions.SearchInDrive(driveService.driveActionTimeOut, query)
if err != nil {
houstonLogger.Error(fmt.Sprintf("Error searching for directory %v: %v", directoryName, err))
return nil, err
}
if len(directoryList.Files) > 0 {
if directoryList.Files[0].Trashed == false {
return directoryList.Files[0], nil
}
}
return driveService.driveActions.CreateDirectory(driveService.driveActionTimeOut, directoryName)
}
func (driveService *Service) moveFilesToDirectory(fileList *drive.FileList, directory *drive.File) ([]string,
error) {
responseMessages := make([]string, 0)
for _, file := range fileList.Files {
// Create a copy of the file in the new directory.
_, err := driveService.driveActions.CopyFile(driveService.driveActionTimeOut, file.Id, directory.Id)
if err != nil {
houstonLogger.Error(fmt.Sprintf("Error copying file %v to directory %v: %v", file.Name, directory.Name, err))
return nil, err
}
// Delete the original file.
err = driveService.driveActions.DeleteFile(driveService.driveActionTimeOut, file.Id)
if err != nil {
houstonLogger.Error(fmt.Sprintf("Error deleting file %v: %v", file.Name, err))
return nil, err
}
}
return responseMessages, nil
}
func (driveService *Service) CollectTranscripts(fileName string) error {
query := fmt.Sprintf("name contains '%s' and mimeType != '%s'", fileName, util.GoogleDriveFileMimeType)
fileList, err := driveService.driveActions.SearchInDrive(driveService.driveActionTimeOut, query)
if err != nil {
houstonLogger.Error(fmt.Sprintf("Error searching for files named %v: %v", fileName, err))
return err
}
if len(fileList.Files) == 0 {
houstonLogger.Info(fmt.Sprintf("No files found named: %v", fileName))
return nil
}
// Check and create a directory with the specified fileName.
directory, err := driveService.createDirectory(fileName)
if err != nil {
return err
}
var filesToMove []drive.File
for _, resultFile := range fileList.Files {
for _, parent := range resultFile.Parents {
if parent != directory.Id {
filesToMove = append(filesToMove, *resultFile)
}
}
}
sort.Slice(filesToMove, func(index1, index2 int) bool {
timeFormat := "2023-10-17T04:30:28.465Z"
time1, err1 := time.Parse(timeFormat, fileList.Files[index1].ModifiedTime)
time2, err2 := time.Parse(timeFormat, fileList.Files[index2].ModifiedTime)
// Handle any parsing errors here if necessary.
if err1 != nil {
houstonLogger.Error(fmt.Sprintf("Error parsing time for object %s: %v", fileList.Files[index1].Name, err1))
return false
}
if err2 != nil {
houstonLogger.Error(fmt.Sprintf("Error parsing time for object %s: %v", fileList.Files[index2].Name, err2))
return false
}
return time1.Before(time2)
})
// Move files to the new directory.
responseMessages, err := driveService.moveFilesToDirectory(fileList, directory)
if err != nil {
houstonLogger.Error(fmt.Sprintf("Error moving files to directory %v: %v", directory.Name, err))
return err
}
houstonLogger.Info(fmt.Sprintf("Response messages: %v", responseMessages))
return nil
}

View File

@@ -0,0 +1,232 @@
package google
import (
"errors"
"github.com/gojuno/minimock/v3"
"github.com/magiconair/properties/assert"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
"google.golang.org/api/drive/v3"
"houston/logger"
"houston/mocks"
"testing"
"time"
)
type DriveServiceSuite struct {
suite.Suite
driveActionTimeOut time.Duration
}
func (suite *DriveServiceSuite) SetupSuite() {
logger.InitLogger()
viper.Set("google.api.timeout", 10*time.Second)
suite.driveActionTimeOut = viper.GetDuration("google.api.timeout")
}
func (suite *DriveServiceSuite) Test_CollectTranscripts_FileNotFound() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut,
"name contains 'directoryName' and mimeType != 'application/vnd."+
"google-apps.folder'").Then(nil, errors.New("file not found"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("directoryName")
assert.Equal(suite.T(), err.Error(), "file not found")
}
func (suite *DriveServiceSuite) Test_CollectTranscripts_SuccessCase() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+
"folder'").Then(&drive.FileList{
Files: []*drive.File{
{
Id: "fileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
},
},
}, nil)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name = 'fileName' and mimeType = 'application/vnd.google-apps.folder'").
Then(&drive.FileList{Files: []*drive.File{}}, nil)
driveActions.CreateDirectoryMock.When(suite.driveActionTimeOut, "fileName").Then(&drive.File{
Id: "directoryId",
Name: "fileName",
MimeType: "application/vnd.google-apps.folder",
Trashed: false,
}, nil)
driveActions.CopyFileMock.When(suite.driveActionTimeOut, "fileId", "directoryId").Then(&drive.File{
Id: "copiedFileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
}, nil)
driveActions.DeleteFileMock.When(suite.driveActionTimeOut, "fileId").Then(nil)
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err, nil)
}
func (suite *DriveServiceSuite) Test_CollectTranscripts_CreateDirectoryError() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+
"folder'").Then(&drive.FileList{
Files: []*drive.File{
{
Id: "fileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
},
},
}, nil)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name = 'fileName' and mimeType = 'application/vnd.google-apps.folder'").
Then(&drive.FileList{Files: []*drive.File{}}, nil)
driveActions.CreateDirectoryMock.When(suite.driveActionTimeOut, "fileName").Then(nil, errors.New("error creating directory"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err.Error(), "error creating directory")
}
func (suite *DriveServiceSuite) Test_CollectTranscripts_CopyFileError() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+
"folder'").Then(&drive.FileList{
Files: []*drive.File{
{
Id: "fileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
},
},
}, nil)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name = 'fileName' and mimeType = 'application/vnd.google-apps.folder'").
Then(&drive.FileList{Files: []*drive.File{}}, nil)
driveActions.CreateDirectoryMock.When(suite.driveActionTimeOut, "fileName").Then(&drive.File{
Id: "directoryId",
Name: "fileName",
MimeType: "application/vnd.google-apps.folder",
Trashed: false,
}, nil)
driveActions.CopyFileMock.When(suite.driveActionTimeOut, "fileId", "directoryId").Then(nil, errors.New("error copying file"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err.Error(), "error copying file")
}
func (suite *DriveServiceSuite) Test_CollectTranscripts_SearchFileError() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+
"folder'").Then(nil, errors.New("error searching for files"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err.Error(), "error searching for files")
}
func (suite *DriveServiceSuite) Test_SearchInDrive_TimeOut() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+"folder'").Then(nil, errors.New("google drive api timedout"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err.Error(), "google drive api timedout")
}
func (suite *DriveServiceSuite) Test_CreateDirectory_TimeOut() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+
"folder'").Then(&drive.FileList{
Files: []*drive.File{
{
Id: "fileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
},
},
}, nil)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name = 'fileName' and mimeType = 'application/vnd.google-apps.folder'").
Then(&drive.FileList{Files: []*drive.File{}}, nil)
driveActions.CreateDirectoryMock.When(suite.driveActionTimeOut, "fileName").Then(nil, errors.New("google drive api timedout"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err.Error(), "google drive api timedout")
}
func (suite *DriveServiceSuite) Test_CopyFile_TimeOut() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+
"folder'").Then(&drive.FileList{
Files: []*drive.File{
{
Id: "fileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
},
},
}, nil)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name = 'fileName' and mimeType = 'application/vnd.google-apps.folder'").
Then(&drive.FileList{Files: []*drive.File{}}, nil)
driveActions.CreateDirectoryMock.When(suite.driveActionTimeOut, "fileName").Then(&drive.File{
Id: "directoryId",
Name: "fileName",
MimeType: "application/vnd.google-apps.folder",
Trashed: false,
}, nil)
driveActions.CopyFileMock.When(suite.driveActionTimeOut, "fileId", "directoryId").Then(nil, errors.New("google drive api timedout"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err.Error(), "google drive api timedout")
}
func (suite *DriveServiceSuite) Test_DeleteFile_TimeOut() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
driveActions := mocks.NewGoogleDriveActionsMock(controller)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name contains 'fileName' and mimeType != 'application/vnd.google-apps."+
"folder'").Then(&drive.FileList{
Files: []*drive.File{
{
Id: "fileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
},
},
}, nil)
driveActions.SearchInDriveMock.When(suite.driveActionTimeOut, "name = 'fileName' and mimeType = 'application/vnd.google-apps.folder'").
Then(&drive.FileList{Files: []*drive.File{}}, nil)
driveActions.CreateDirectoryMock.When(suite.driveActionTimeOut, "fileName").Then(&drive.File{
Id: "directoryId",
Name: "fileName",
MimeType: "application/vnd.google-apps.folder",
Trashed: false,
}, nil)
driveActions.CopyFileMock.When(suite.driveActionTimeOut, "fileId", "directoryId").Then(&drive.File{
Id: "copiedFileId",
Name: "fileName",
MimeType: "application/vnd.google-apps.document",
}, nil)
driveActions.DeleteFileMock.When(suite.driveActionTimeOut, "fileId").Then(errors.New("google drive api timedout"))
driveService := NewDriveService(driveActions)
err := driveService.CollectTranscripts("fileName")
assert.Equal(suite.T(), err.Error(), "google drive api timedout")
}
func TestDriveService(t *testing.T) {
suite.Run(t, new(DriveServiceSuite))
}

View File

@@ -1,16 +1,12 @@
package incident package incident
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
slackClient "github.com/slack-go/slack" slackClient "github.com/slack-go/slack"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
"gorm.io/gorm" "gorm.io/gorm"
"houston/common/util" "houston/common/util"
"houston/internal/processor/action/view" "houston/internal/processor/action/view"
@@ -20,6 +16,8 @@ import (
"houston/model/severity" "houston/model/severity"
"houston/model/team" "houston/model/team"
"houston/model/user" "houston/model/user"
conference2 "houston/pkg/conference"
service2 "houston/service/conference"
request "houston/service/request" request "houston/service/request"
service "houston/service/response" service "houston/service/response"
"houston/service/slack" "houston/service/slack"
@@ -361,17 +359,23 @@ func createIncidentWorkflow(
} }
} }
if viper.GetBool("ENABLE_GMEET") { if viper.GetBool("ENABLE_CONFERENCE") {
gmeet, err := createGmeetLink(channel.Name) calendarActions := conference2.GetCalendarActions()
calendarService := service2.NewCalendarService(calendarActions)
calendarEvent, err := calendarService.CreateEvent(channel.Name)
conferenceLink := calendarEvent.ConferenceLink
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("%s [%s] Error while creating gmeet", logTag, incidentName), zap.Error(err)) logger.Error(fmt.Sprintf("%s [%s] Error while creating conference", logTag, incidentName), zap.Error(err))
} else { } else {
_, err := i.slackService.PostMessage(fmt.Sprint("gmeet: ", gmeet), false, channel) util.UpdateIncidentWithConferenceDetails(incidentEntity, calendarEvent, i.incidentRepository)
//ToDo Update the summary in slack channel with Conference link. To be done post update incident refactoring
i.slackService.AddBookmark(conferenceLink, channel, calendarService.GetConferenceTitle())
_, err := i.slackService.PostMessage(fmt.Sprintf(util.ConferenceMessage, conferenceLink), false, channel)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("%s [%s] Failed to post Google Meet link: %s", logTag, incidentName, gmeet)) logger.Error(fmt.Sprintf("%s [%s] Failed to post Conference link: %s", logTag, incidentName, conferenceLink))
} }
logger.Info(fmt.Sprintf("%s [%s] Conference link posted to the channel %s", logTag, incidentName, conferenceLink))
} }
logger.Info(fmt.Sprintf("%s [%s] Google Meeting link posted to the channel %s", logTag, incidentName, gmeet))
} }
return nil return nil
} }
@@ -727,48 +731,3 @@ func postInWebhookSlackChannel(
return return
} }
} }
func createGmeetLink(channelName string) (string, error) {
incidentName := channelName
calclient, err := calendar.NewService(context.Background(), option.WithCredentialsFile(viper.GetString("GMEET_CONFIG_FILE_PATH")))
if err != nil {
logger.Error(
fmt.Sprintf("%s [%s]Calander service creation failed, unable to read client secret file: ", logTag, incidentName),
zap.Error(err),
)
return "", err
}
t0 := time.Now().Format(time.RFC3339)
t1 := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
event := &calendar.Event{
Summary: channelName,
Description: "Incident",
Start: &calendar.EventDateTime{
DateTime: t0,
},
End: &calendar.EventDateTime{
DateTime: t1,
},
ConferenceData: &calendar.ConferenceData{
CreateRequest: &calendar.CreateConferenceRequest{
RequestId: uuid.NewString(),
ConferenceSolutionKey: &calendar.ConferenceSolutionKey{
Type: "hangoutsMeet",
},
Status: &calendar.ConferenceRequestStatus{
StatusCode: "success",
},
},
},
}
calendarID := "primary" //use "primary"
event, err = calclient.Events.Insert(calendarID, event).ConferenceDataVersion(1).Do()
if err != nil {
logger.Error("Unable to create event. %v\n", zap.Error(err))
return "", err
}
calclient.Events.Delete(calendarID, event.Id).Do()
return event.HangoutLink, nil
}

View File

@@ -37,6 +37,15 @@ func (s *SlackService) PostMessage(message string, escape bool, channel *slack.C
return timeStamp, nil return timeStamp, nil
} }
func (s *SlackService) AddBookmark(bookmark string, channel *slack.Channel, title string) {
bookmarkParam := slack.AddBookmarkParameters{Link: bookmark, Title: title}
_, err := s.SocketModeClient.AddBookmark(channel.ID, bookmarkParam)
if err != nil {
e := fmt.Sprintf("%s Failed to add book mark into channel: %s", logTag, channel.Name)
logger.Error(e, zap.Error(err))
}
}
func (s *SlackService) PostMessageOption(channelID string, messageOption slack.MsgOption) (string, error) { func (s *SlackService) PostMessageOption(channelID string, messageOption slack.MsgOption) (string, error) {
_, timeStamp, err := s.SocketModeClient.PostMessage(channelID, messageOption) _, timeStamp, err := s.SocketModeClient.PostMessage(channelID, messageOption)
if err != nil { if err != nil {