diff --git a/.gitignore b/.gitignore index f784377..488e807 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ /.idea .idea /out +/mocks +*_mock.go go.sum diff --git a/Makefile b/Makefile index 2e8bfbd..f7a8c74 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,11 @@ -.PHONY: build -build: - go mod tidy && CGO_ENABLED=0 go build -ldflags="-s -w" -o houston cmd/main.go +.PHONY: docker-run +docker-run: docker-build + docker run houston .PHONY: docker-build docker-build: docker build -t houston -f Dockerfile.houston . -.PHONY: docker-run -docker-run: docker-build - docker run houston - .PHONY: migration-up migration-up: migrate -path db/migration/ -database "${POSTGRES_DSN}?sslmode=disable" -verbose up @@ -17,3 +13,23 @@ migration-up: .PHONY: migration-down migration-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 diff --git a/common/util/constant.go b/common/util/constant.go index 328d117..6abf4cd 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -45,3 +45,11 @@ const ( AlreadyArchivedError = "already_archived" NotInChannelError = "not_in_channel" ) + +const ( + GoogleDriveFileMimeType = "application/vnd.google-apps.folder" +) + +const ( + ConferenceMessage = "To discuss, use this *<%s|Meet link>*" +) diff --git a/common/util/incident_helper.go b/common/util/incident_helper.go index e157c6a..83d96a9 100644 --- a/common/util/incident_helper.go +++ b/common/util/incident_helper.go @@ -9,6 +9,7 @@ import ( "houston/model/incident" "houston/model/severity" "houston/model/team" + "houston/pkg/conference" "houston/pkg/slackbot" "strings" "time" @@ -90,3 +91,12 @@ func getOncallOrResponderHandle(teamEntity *team.TeamEntity, severityEntity *sev func isPseIncident(teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity) bool { 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())) + } +} diff --git a/config/application.properties b/config/application.properties index fca96c1..7754cd4 100644 --- a/config/application.properties +++ b/config/application.properties @@ -58,9 +58,12 @@ s3.bucket.name=S3_BUCKET_NAME s3.access.key=S3_ACCESS_KEY s3.secret.key=S3_SECRET_KEY -#gmeet -gmeet.enable=ENABLE_GMEET -gmeet.config.file.path=GMEET_CONFIG_FILE_PATH +#conference +conference.enable=ENABLE_CONFERENCE +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.sa.baseurl=GOCD_SA_BASEURL diff --git a/db/migration/000005_add_incident_columns.up.sql b/db/migration/000005_add_incident_columns.up.sql new file mode 100644 index 0000000..88a3678 --- /dev/null +++ b/db/migration/000005_add_incident_columns.up.sql @@ -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); \ No newline at end of file diff --git a/go.mod b/go.mod index c96048b..f449923 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/chromedp/chromedp v0.9.1 github.com/gin-contrib/zap v0.1.0 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/jackc/pgx/v5 v5.3.1 github.com/joho/godotenv v1.5.1 @@ -17,9 +18,11 @@ require ( github.com/slack-go/slack v0.12.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.3 github.com/thoas/go-funk v0.9.3 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d + golang.org/x/oauth2 v0.13.0 gorm.io/datatypes v1.2.0 gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.2 @@ -47,6 +50,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.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/go-sql-driver/mysql v1.7.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/mailru/easyjson v0.7.7 // 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/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.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/genproto 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 gorm.io/driver/mysql v1.4.7 // indirect @@ -91,7 +94,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.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/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/internal/clients/google.go b/internal/clients/google.go new file mode 100644 index 0000000..8890bfa --- /dev/null +++ b/internal/clients/google.go @@ -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()) +} diff --git a/internal/processor/action/incident_mark_duplicate_action.go b/internal/processor/action/incident_mark_duplicate_action.go index a8af5a0..5ada883 100644 --- a/internal/processor/action/incident_mark_duplicate_action.go +++ b/internal/processor/action/incident_mark_duplicate_action.go @@ -130,7 +130,7 @@ func (dip *DuplicateIncidentAction) DuplicateIncidentProcess(callback slack.Inte } } }() - + //ToDo() Delete Conference event if exists and if incident is duplicated } else { 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) diff --git a/internal/processor/action/incident_resolve_action.go b/internal/processor/action/incident_resolve_action.go index 9a5a819..237813c 100644 --- a/internal/processor/action/incident_resolve_action.go +++ b/internal/processor/action/incident_resolve_action.go @@ -116,6 +116,8 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact } msgUpdate := NewIncidentChannelMessageUpdateAction(irp.client, irp.incidentService, irp.teamRepository, irp.severityRepository) msgUpdate.ProcessAction(incidentEntity.SlackChannel) + //ToDo() Delete Conference event if exists and if incident is resolved + go func() { if incidentEntity.SeverityId != incident.Sev0Id && incidentEntity.SeverityId != incident.Sev1Id { postErr := util.PostArchivingTimeToIncidentChannel(channelId, incident.Resolved, irp.client) diff --git a/internal/processor/action/start_incident_modal_submission_action.go b/internal/processor/action/start_incident_modal_submission_action.go index c1cfcba..a8a0e8b 100644 --- a/internal/processor/action/start_incident_modal_submission_action.go +++ b/internal/processor/action/start_incident_modal_submission_action.go @@ -1,7 +1,6 @@ package action import ( - "context" "encoding/json" "fmt" "gorm.io/gorm" @@ -11,19 +10,18 @@ import ( "houston/model/incident" "houston/model/severity" "houston/model/team" + conference2 "houston/pkg/conference" "houston/pkg/slackbot" + "houston/service/conference" incidentService "houston/service/incident" request "houston/service/request" "strings" "time" - "github.com/google/uuid" "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "github.com/spf13/viper" "go.uber.org/zap" - "google.golang.org/api/calendar/v3" - "google.golang.org/api/option" incidentHelper "houston/common/util" ) @@ -36,14 +34,7 @@ type CreateIncidentAction struct { db *gorm.DB } -func NewCreateIncidentProcessor( - client *socketmode.Client, - incidentService *incident.Repository, - teamService *team.Repository, - severityService *severity.Repository, - slackbotClient *slackbot.Client, - db *gorm.DB, -) *CreateIncidentAction { +func NewCreateIncidentProcessor(client *socketmode.Client, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository, slackbotClient *slackbot.Client, db *gorm.DB) *CreateIncidentAction { return &CreateIncidentAction{ client: client, incidentRepository: incidentService, @@ -123,16 +114,29 @@ func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessing(callback s return } } - if viper.GetBool("ENABLE_GMEET") { - gmeet, err := createGmeetLink(*channelID) + if viper.GetBool("ENABLE_CONFERENCE") { + calendarActions := conference2.GetCalendarActions() + calendarService := conference.NewCalendarService(calendarActions) + calendarEvent, err := calendarService.CreateEvent(incidentEntity.IncidentName) 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 { - msgOption := slack.MsgOptionText(fmt.Sprintf("gmeet: ", gmeet), false) - isp.client.PostMessage(*channelID, msgOption) + util.UpdateIncidentWithConferenceDetails(incidentEntity, calendarEvent, isp.incidentRepository) + 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 @@ -179,47 +183,6 @@ func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessingV2( 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) { var channelName string if viper.GetString("env") != "prod" { diff --git a/internal/processor/action/view/incident_summary_section.go b/internal/processor/action/view/incident_summary_section.go index 19afe2e..6ac163b 100644 --- a/internal/processor/action/view/incident_summary_section.go +++ b/internal/processor/action/view/incident_summary_section.go @@ -17,7 +17,7 @@ func IncidentSummarySection(incident *incident.IncidentEntity, team *team.TeamEn buildDescriptionBlock(incident.Description), buildTypeAndChannelSectionBlock(incident, team.Name), buildSeverityAndTicketSectionBlock(severity.Name), - buildStatusAndMeetLinkSectionBlock(incidentStatus.Name), + buildStatusAndConferenceLinkSectionBlock(incidentStatus.Name, incident.ConferenceLink), buildCreatedByAndCreatedAtSectionBlock(incident), }, } @@ -62,10 +62,14 @@ func buildSeverityAndTicketSectionBlock(severityName string) *slack.SectionBlock 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{ 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) diff --git a/model/incident/entity.go b/model/incident/entity.go index 38e7c9a..faa9d8a 100644 --- a/model/incident/entity.go +++ b/model/incident/entity.go @@ -63,6 +63,8 @@ type IncidentEntity struct { UpdatedBy string `gorm:"column:updated_by"` MetaData JSON `gorm:"column:meta_data"` RCA string `gorm:"column:rca_text"` + ConferenceId string `gorm:"column:conference_id"` + ConferenceLink string `gorm:"column:conference_link"` } func (IncidentEntity) TableName() string { diff --git a/pkg/conference/calendar_interface.go b/pkg/conference/calendar_interface.go new file mode 100644 index 0000000..8b59571 --- /dev/null +++ b/pkg/conference/calendar_interface.go @@ -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 +} diff --git a/pkg/conference/google_calendar_actions.go b/pkg/conference/google_calendar_actions.go new file mode 100644 index 0000000..23e9127 --- /dev/null +++ b/pkg/conference/google_calendar_actions.go @@ -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 +} diff --git a/pkg/google/googleDrive/drive_actions.go b/pkg/google/googleDrive/drive_actions.go new file mode 100644 index 0000000..f44308f --- /dev/null +++ b/pkg/google/googleDrive/drive_actions.go @@ -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 +} diff --git a/pkg/google/googleDrive/drive_interface.go b/pkg/google/googleDrive/drive_interface.go new file mode 100644 index 0000000..56d6e07 --- /dev/null +++ b/pkg/google/googleDrive/drive_interface.go @@ -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 +} diff --git a/service/conference/calendar_service.go b/service/conference/calendar_service.go new file mode 100644 index 0000000..9b13e4b --- /dev/null +++ b/service/conference/calendar_service.go @@ -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() +} diff --git a/service/conference/calendar_service_test.go b/service/conference/calendar_service_test.go new file mode 100644 index 0000000..42e60e1 --- /dev/null +++ b/service/conference/calendar_service_test.go @@ -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)) +} diff --git a/service/google/drive_service.go b/service/google/drive_service.go new file mode 100644 index 0000000..eb5ec8d --- /dev/null +++ b/service/google/drive_service.go @@ -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 +} diff --git a/service/google/drive_service_test.go b/service/google/drive_service_test.go new file mode 100644 index 0000000..12e66dd --- /dev/null +++ b/service/google/drive_service_test.go @@ -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)) +} diff --git a/service/incident/incident_service_v2.go b/service/incident/incident_service_v2.go index c58bcc0..85c329f 100644 --- a/service/incident/incident_service_v2.go +++ b/service/incident/incident_service_v2.go @@ -1,16 +1,12 @@ package incident import ( - "context" "encoding/json" "errors" "fmt" - "github.com/google/uuid" slackClient "github.com/slack-go/slack" "github.com/spf13/viper" "go.uber.org/zap" - "google.golang.org/api/calendar/v3" - "google.golang.org/api/option" "gorm.io/gorm" "houston/common/util" "houston/internal/processor/action/view" @@ -20,6 +16,8 @@ import ( "houston/model/severity" "houston/model/team" "houston/model/user" + conference2 "houston/pkg/conference" + service2 "houston/service/conference" request "houston/service/request" service "houston/service/response" "houston/service/slack" @@ -361,17 +359,23 @@ func createIncidentWorkflow( } } - if viper.GetBool("ENABLE_GMEET") { - gmeet, err := createGmeetLink(channel.Name) + if viper.GetBool("ENABLE_CONFERENCE") { + calendarActions := conference2.GetCalendarActions() + calendarService := service2.NewCalendarService(calendarActions) + calendarEvent, err := calendarService.CreateEvent(channel.Name) + conferenceLink := calendarEvent.ConferenceLink 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 { - _, 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 { - 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 } @@ -727,48 +731,3 @@ func postInWebhookSlackChannel( 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 -} diff --git a/service/slack/slack_service.go b/service/slack/slack_service.go index 83ecdc3..40dfe92 100644 --- a/service/slack/slack_service.go +++ b/service/slack/slack_service.go @@ -37,6 +37,15 @@ func (s *SlackService) PostMessage(message string, escape bool, channel *slack.C 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) { _, timeStamp, err := s.SocketModeClient.PostMessage(channelID, messageOption) if err != nil {