diff --git a/internal/bot/event_app_home.go b/internal/bot/event_app_home.go index 539eaa2..92565f2 100644 --- a/internal/bot/event_app_home.go +++ b/internal/bot/event_app_home.go @@ -60,7 +60,7 @@ func HandleAppHomeEvent(ctx context.Context, client *slack.Client, db *gorm.DB, } if _, err = client.PublishViewContext(ctx, p.UserID, view, ""); err != nil { - return err + return errors.Wrap(err, "failed to publish AppHome view") } return nil diff --git a/internal/bot/job_check_pair_test.go b/internal/bot/job_check_pair_test.go index 38c47a3..6a78718 100644 --- a/internal/bot/job_check_pair_test.go +++ b/internal/bot/job_check_pair_test.go @@ -212,7 +212,7 @@ func Test_HandleCheckPairButtons(t *testing.T) { err := json.NewDecoder(r.Body).Decode(&webhook) assert.Nil(t, err) - assert.Len(t, webhook.Blocks.BlockSet, 2) + assert.Len(t, webhook.Blocks.BlockSet, 3) assert.True(t, webhook.ReplaceOriginal) // Assert that the response matches the right template diff --git a/internal/bot/job_create_matches_test.go b/internal/bot/job_create_matches_test.go index 67565a3..cc573de 100644 --- a/internal/bot/job_create_matches_test.go +++ b/internal/bot/job_create_matches_test.go @@ -116,6 +116,76 @@ func (s *CreateMatchesSuite) Test_CreateMatches() { r.Equal(int64(1), count) } +func (s *CreateMatchesSuite) Test_CreateMatches_SameGenderTwoParticipants() { + r := require.New(s.T()) + + resource, databaseURL, err := database.NewTestPostgresDB(false) + r.NoError(err) + defer resource.Close() + + r.NoError(database.Migrate(databaseURL)) + + db, err := database.NewGormDB(databaseURL) + r.NoError(err) + + channelID := "C0123456789" + + // Write channel to the database + db.Create(&models.Channel{ + ChannelID: channelID, + Inviter: "U9876543210", + ConnectionMode: models.VirtualConnectionMode, + Interval: models.Biweekly, + Weekday: time.Friday, + Hour: 12, + NextRound: time.Now().Add(24 * time.Hour), + }) + + // Add members to the database + members := []struct { + userID string + gender models.Gender + isActive bool + hasGenderPreference bool + }{ + {"U0123456789", models.Male, true, false}, + {"U8765432109", models.Male, true, true}, + } + + for _, member := range members { + db.Create(&models.Member{ + ChannelID: channelID, + UserID: member.userID, + Gender: member.gender, + IsActive: &member.isActive, + HasGenderPreference: &member.hasGenderPreference, + }) + } + + // Write a record in the rounds table + db.Create(&models.Round{ + ChannelID: channelID, + }) + + // Test + err = CreateMatches(s.ctx, db, nil, &CreateMatchesParams{ + ChannelID: channelID, + RoundID: 1, + }) + r.NoError(err) + r.Contains(s.buffer.String(), "added new match to the database") + r.Contains(s.buffer.String(), "paired active participants for chat-roulette") + r.Contains(s.buffer.String(), "participants=2") + r.Contains(s.buffer.String(), "pairs=1") + r.Contains(s.buffer.String(), "unpaired=0") + + // Verify matches + var count int64 + result := db.Model(&models.Job{}).Where("job_type = ?", models.JobTypeCreatePair).Count(&count) + r.NoError(result.Error) + r.Equal(int64(1), count) +} + func (s *CreateMatchesSuite) Test_QueueCreateMatchesJob() { r := require.New(s.T()) diff --git a/internal/bot/job_greet_admin.go b/internal/bot/job_greet_admin.go index eb9efb2..088bc10 100644 --- a/internal/bot/job_greet_admin.go +++ b/internal/bot/job_greet_admin.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "net/http" + "net/url" + "path" "time" "github.com/hashicorp/go-hclog" @@ -158,7 +160,7 @@ func HandleGreetAdminButton(ctx context.Context, client *slack.Client, interacti // RenderOnboardingChannelView renders the view template for collecting settings // to enable a new chat-roulette channel. -func RenderOnboardingChannelView(ctx context.Context, interaction *slack.InteractionCallback) ([]byte, error) { +func RenderOnboardingChannelView(ctx context.Context, interaction *slack.InteractionCallback, baseURL string) ([]byte, error) { // Start new span tracer := otel.Tracer("") _, span := tracer.Start(ctx, "render.channel") @@ -171,9 +173,16 @@ func RenderOnboardingChannelView(ctx context.Context, interaction *slack.Interac } // Render the template + u, err := url.Parse(baseURL) + if err != nil { + return nil, errors.Wrap(err, "failed to parse base URL") + } + u.Path = path.Join(u.Path, "static/img/coffee-machine.jpg") + t := onboardingTemplate{ ChannelID: pm.ChannelID, PrivateMetadata: interaction.View.PrivateMetadata, + ImageURL: u.String(), } content, err := renderTemplate(onboardingChannelTemplateFilename, t) @@ -237,7 +246,7 @@ func RespondGreetAdminWebhook(ctx context.Context, client *http.Client, interact return errors.Wrap(err, "failed to decode base64 string to privateMetadata") } - confirmationText := `*Chat Roulette is now enabled! I hope you enjoy using this app* :smile:` + confirmationText := `*Chat Roulette is now enabled! I hope you enjoy using this app* :grin:` text := slack.NewTextBlockObject("mrkdwn", confirmationText, false, false) section := slack.NewSectionBlock(text, nil, nil) diff --git a/internal/bot/job_greet_admin_test.go b/internal/bot/job_greet_admin_test.go index 3b6ac2c..d7bb84f 100644 --- a/internal/bot/job_greet_admin_test.go +++ b/internal/bot/job_greet_admin_test.go @@ -261,7 +261,7 @@ b3N0L2FjdGlvbnMvYS9iL2MifQ==`, }, } - content, err := RenderOnboardingChannelView(context.Background(), interaction) + content, err := RenderOnboardingChannelView(context.Background(), interaction, "http://localhost/") assert.Nil(t, err) assert.NotNil(t, content) diff --git a/internal/bot/job_greet_member.go b/internal/bot/job_greet_member.go index 706c60b..a5558a0 100644 --- a/internal/bot/job_greet_member.go +++ b/internal/bot/job_greet_member.go @@ -232,7 +232,12 @@ func UpsertMemberLocationInfo(ctx context.Context, db *gorm.DB, interaction *sla // Extract the values from the view state country := interaction.View.State.Values["onboarding-country"]["onboarding-location-country"].SelectedOption.Value - city := templatex.Capitalize(interaction.View.State.Values["onboarding-city"]["onboarding-location-city"].Value) + + city := strings.TrimSpace( + templatex.Capitalize( + interaction.View.State.Values["onboarding-city"]["onboarding-location-city"].Value, + ), + ) // Schedule an UPDATE_MEMBER job to update the member's location. // UpdateMember() could be called directly here, however @@ -332,7 +337,7 @@ func RenderOnboardingGenderView(ctx context.Context, interaction *slack.Interact if err != nil { return nil, errors.Wrap(err, "failed to parse base URL") } - u.Path = path.Join(u.Path, "static/img/social-icons.png") + u.Path = path.Join(u.Path, "static/img/gender.jpg") // Render the template t := onboardingTemplate{ @@ -367,7 +372,7 @@ func UpsertMemberGenderInfo(ctx context.Context, db *gorm.DB, interaction *slack gender := interaction.View.State.Values["onboarding-gender-select"]["onboarding-gender-select"].SelectedOption.Value hasGenderPreference := false - if len(interaction.View.State.Values["onboarding-gender-checkbox"]["onboarding-gender-checkbox"].SelectedOptions) > 0 { + if len(interaction.View.State.Values["onboarding-gender-checkbox"]["onboarding-gender-checkbox"].SelectedOptions) != 0 { hasGenderPreference = true } @@ -378,7 +383,7 @@ func UpsertMemberGenderInfo(ctx context.Context, db *gorm.DB, interaction *slack UserID: interaction.User.ID, ChannelID: pm.ChannelID, Gender: gender, - HasGenderPreference: hasGenderPreference, + HasGenderPreference: &hasGenderPreference, } if err := QueueUpdateMemberJob(ctx, db, p); err != nil { @@ -460,7 +465,12 @@ func UpsertMemberProfileInfo(ctx context.Context, db *gorm.DB, interaction *slac // Extract the values from the view state profileType := interaction.View.State.Values["onboarding-profile-type"]["onboarding-profile-type"].SelectedOption.Value - profileLink := interaction.View.State.Values["onboarding-profile-link"]["onboarding-profile-link"].Value + + profileLink := strings.ToLower( + strings.TrimSpace( + interaction.View.State.Values["onboarding-profile-link"]["onboarding-profile-link"].Value, + ), + ) // Schedule an UPDATE_MEMBER job to update the member's location. // UpdateMember() could be called directly here, however @@ -470,7 +480,38 @@ func UpsertMemberProfileInfo(ctx context.Context, db *gorm.DB, interaction *slac ChannelID: pm.ChannelID, ProfileType: sqlcrypter.NewEncryptedBytes(profileType), ProfileLink: sqlcrypter.NewEncryptedBytes(profileLink), - IsActive: true, + } + + if err := QueueUpdateMemberJob(ctx, db, p); err != nil { + return errors.Wrap(err, "failed to add UPDATE_MEMBER job to the queue") + } + + return nil +} + +// SetMemberIsActive marks a new member as active in the database +// after they have completed the onboarding flow. +func SetMemberIsActive(ctx context.Context, db *gorm.DB, interaction *slack.InteractionCallback) error { + // Start new span + tracer := otel.Tracer("") + ctx, span := tracer.Start(ctx, "upsert.is_active") + defer span.End() + + // Extract the ChannelID from the private_metadata field + var pm privateMetadata + if err := pm.Decode(interaction.View.PrivateMetadata); err != nil { + return errors.Wrap(err, "failed to decode base64 string to privateMetadata") + } + + // Schedule an UPDATE_MEMBER job to update the member's location. + // UpdateMember() could be called directly here, however + // scheduling a background job will ensure it is reliably executed. + isActive := true + + p := &UpdateMemberParams{ + UserID: interaction.User.ID, + ChannelID: pm.ChannelID, + IsActive: &isActive, } if err := QueueUpdateMemberJob(ctx, db, p); err != nil { @@ -553,7 +594,6 @@ func UpsertMemberCalendlyLink(ctx context.Context, db *gorm.DB, interaction *sla UserID: interaction.User.ID, ChannelID: pm.ChannelID, CalendlyLink: sqlcrypter.NewEncryptedBytes(calendlyLink), - IsActive: true, } if err := QueueUpdateMemberJob(ctx, db, p); err != nil { diff --git a/internal/bot/job_greet_member_test.go b/internal/bot/job_greet_member_test.go index e2f7d1e..a5a15a6 100644 --- a/internal/bot/job_greet_member_test.go +++ b/internal/bot/job_greet_member_test.go @@ -500,13 +500,15 @@ b3N0L2FjdGlvbnMvYS9iL2MifQ==`, db, mock := database.NewMockedGormDB() + hasGenderPreference := true + database.MockQueueJob( mock, &UpdateMemberParams{ ChannelID: "C0123456789", UserID: userID, Gender: models.Male.String(), - HasGenderPreference: true, + HasGenderPreference: &hasGenderPreference, }, models.JobTypeUpdateMember.String(), models.JobPriorityHigh, @@ -577,7 +579,6 @@ b3N0L2FjdGlvbnMvYS9iL2MifQ==`, UserID: userID, ProfileType: sqlcrypter.NewEncryptedBytes(profileType), ProfileLink: sqlcrypter.NewEncryptedBytes(profileLink), - IsActive: true, }, models.JobTypeUpdateMember.String(), models.JobPriorityHigh, @@ -774,7 +775,6 @@ b3N0L2FjdGlvbnMvYS9iL2MifQ==`, ChannelID: "C0123456789", UserID: userID, CalendlyLink: sqlcrypter.NewEncryptedBytes(calendlyLink), - IsActive: true, }, models.JobTypeUpdateMember.String(), models.JobPriorityHigh, @@ -784,6 +784,10 @@ b3N0L2FjdGlvbnMvYS9iL2MifQ==`, assert.Nil(t, err) } +func Test_SetMemberIsActive(t *testing.T) { + // TODO +} + func Test_RespondGreetMemberWebhook(t *testing.T) { userID := "U0123456789" channelID := "C9876543210" diff --git a/internal/bot/job_notify_pair.go b/internal/bot/job_notify_pair.go index a483f51..9549f94 100644 --- a/internal/bot/job_notify_pair.go +++ b/internal/bot/job_notify_pair.go @@ -138,7 +138,9 @@ func NotifyPair(ctx context.Context, db *gorm.DB, client *slack.Client, p *Notif // We can marshal the template into View as it contains Blocks var view slack.View if err := json.Unmarshal([]byte(content), &view); err != nil { - return errors.Wrap(err, "failed to marshal JSON") + message := "failed to marshal JSON" + logger.Error(message, "error", err) + return errors.Wrap(err, message) } // Send the Slack group message to the pair diff --git a/internal/bot/job_notify_pair_test.go b/internal/bot/job_notify_pair_test.go index 411be85..325203a 100644 --- a/internal/bot/job_notify_pair_test.go +++ b/internal/bot/job_notify_pair_test.go @@ -47,7 +47,7 @@ func Test_notifyPairTemplate(t *testing.T) { UserID: "U0123456789", Country: sqlcrypter.NewEncryptedBytes("Kenya"), City: sqlcrypter.NewEncryptedBytes("Nairobi"), - ProfileType: sqlcrypter.NewEncryptedBytes("Github"), + ProfileType: sqlcrypter.NewEncryptedBytes("GitHub"), ProfileLink: sqlcrypter.NewEncryptedBytes("https://github.com/AhmedARmohamed"), CalendlyLink: sqlcrypter.NewEncryptedBytes("https://calendly.com/AhmedARmohamed"), }, @@ -56,7 +56,7 @@ func Test_notifyPairTemplate(t *testing.T) { UserID: "U9876543210", Country: sqlcrypter.NewEncryptedBytes("United States"), City: sqlcrypter.NewEncryptedBytes("Phoenix"), - ProfileType: sqlcrypter.NewEncryptedBytes("Github"), + ProfileType: sqlcrypter.NewEncryptedBytes("GitHub"), ProfileLink: sqlcrypter.NewEncryptedBytes("https://github.com/bincyber"), }, PartnerTimezone: "MST (UTC-07:00)", @@ -123,7 +123,7 @@ func (s *NotifyPairSuite) Test_NotifyPair() { db.Create(&models.Channel{ ChannelID: channelID, Inviter: "U9876543210", - ConnectionMode: models.VirtualConnectionMode, + ConnectionMode: models.PhysicalConnectionMode, Interval: models.Biweekly, Weekday: time.Friday, Hour: 12, @@ -142,6 +142,9 @@ func (s *NotifyPairSuite) Test_NotifyPair() { HasGenderPreference: new(bool), Country: sqlcrypter.NewEncryptedBytes("Canada"), City: sqlcrypter.NewEncryptedBytes("Toronto"), + ProfileType: sqlcrypter.NewEncryptedBytes("GitHub"), + ProfileLink: sqlcrypter.NewEncryptedBytes("github.com/user1"), + CalendlyLink: sqlcrypter.NewEncryptedBytes("https://calendly.com/example"), }) secondUserID := "U5555666778" @@ -153,6 +156,8 @@ func (s *NotifyPairSuite) Test_NotifyPair() { HasGenderPreference: new(bool), Country: sqlcrypter.NewEncryptedBytes("United Kingdom"), City: sqlcrypter.NewEncryptedBytes("Manchester"), + ProfileType: sqlcrypter.NewEncryptedBytes("Twitter"), + ProfileLink: sqlcrypter.NewEncryptedBytes("twitter.com/user2"), }) // Write records in the rounds and matches table @@ -186,15 +191,16 @@ func (s *NotifyPairSuite) Test_NotifyPair() { w.Write([]byte(`{"ok":false}`)) } - r.Len(blocks.BlockSet, 8) + r.Len(blocks.BlockSet, 12) - participantSection, ok := blocks.BlockSet[5].(*slack.SectionBlock) + // Verify + participantSection, ok := blocks.BlockSet[4].(*slack.SectionBlock) r.True(ok) - r.Contains(participantSection.Text.Text, "*Name:* <@U0123456789>\n*Location:* Toronto, Canada") + r.Contains(participantSection.Text.Text, fmt.Sprintf(":identification_card: *Name:* <@%s>", firstUserID)) - partnerSection, ok := blocks.BlockSet[7].(*slack.SectionBlock) + partnerSection, ok := blocks.BlockSet[8].(*slack.SectionBlock) r.True(ok) - r.Contains(partnerSection.Text.Text, "*Name:* <@U5555666778>\n*Location:* Manchester, United Kingdom") + r.Contains(partnerSection.Text.Text, fmt.Sprintf(":identification_card: *Name:* <@%s>", secondUserID)) w.Write([]byte(`{ "ok": true, @@ -208,7 +214,6 @@ func (s *NotifyPairSuite) Test_NotifyPair() { url := fmt.Sprintf("%s/", httpServer.URL) client := slack.New("xoxb-test-token-here", slack.OptionAPIURL(url)) - // Test err = NotifyPair(s.ctx, db, client, &NotifyPairParams{ ChannelID: channelID, MatchID: 1, diff --git a/internal/bot/job_update_member.go b/internal/bot/job_update_member.go index c1af7d8..1a6fe41 100644 --- a/internal/bot/job_update_member.go +++ b/internal/bot/job_update_member.go @@ -25,8 +25,8 @@ type UpdateMemberParams struct { ProfileType sqlcrypter.EncryptedBytes `json:"profile_type,omitempty"` ProfileLink sqlcrypter.EncryptedBytes `json:"profile_link,omitempty"` CalendlyLink sqlcrypter.EncryptedBytes `json:"calendly_link,omitempty"` - IsActive bool `json:"is_active"` - HasGenderPreference bool `json:"has_gender_preference"` + IsActive *bool `json:"is_active,omitempty"` + HasGenderPreference *bool `json:"has_gender_preference,omitempty"` } // UpdateMember updates the participation status for a member of a Slack channel. @@ -39,6 +39,9 @@ func UpdateMember(ctx context.Context, db *gorm.DB, client *slack.Client, p *Upd logger.Info("updating member") + // Normalize strings - TODO + // city := templatex.Capitalize(p.City.String()) // Only necessary if not nil + // Update the row for the member in the database member := models.Member{ UserID: p.UserID, @@ -49,8 +52,8 @@ func UpdateMember(ctx context.Context, db *gorm.DB, client *slack.Client, p *Upd ProfileType: p.ProfileType, ProfileLink: p.ProfileLink, CalendlyLink: p.CalendlyLink, - HasGenderPreference: &p.HasGenderPreference, - IsActive: &p.IsActive, + HasGenderPreference: p.HasGenderPreference, + IsActive: p.IsActive, } if p.Gender != "" { diff --git a/internal/bot/job_update_member_test.go b/internal/bot/job_update_member_test.go index 9c34b03..47ba3db 100644 --- a/internal/bot/job_update_member_test.go +++ b/internal/bot/job_update_member_test.go @@ -5,8 +5,10 @@ import ( "encoding/json" "fmt" "testing" + "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/bincyber/go-sqlcrypter" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" @@ -21,40 +23,72 @@ func Test_UpdateMember(t *testing.T) { logger, out := o11y.NewBufferedLogger() ctx := hclog.WithContext(context.Background(), logger) - db, mock := database.NewMockedGormDB() + resource, databaseURL, err := database.NewTestPostgresDB(false) + r.NoError(err) + defer resource.Close() + + r.NoError(database.Migrate(databaseURL)) + + db, err := database.NewGormDB(databaseURL) + r.NoError(err) + + sqlcrypter.Init(database.NoOpCrypter{}) channelID := "C9876543210" userID := "U0123456789" - mock.ExpectBegin() - mock.ExpectExec(`UPDATE "members" SET .* WHERE channel_id = (.+) AND user_id = (.+)`). - WithArgs( - userID, - channelID, - models.Male.String(), - true, - true, - database.AnyTime(), - channelID, - userID, - ). - WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() + // Write channel to the database + db.Create(&models.Channel{ + ChannelID: channelID, + Inviter: "U9876543210", + ConnectionMode: models.VirtualConnectionMode, + Interval: models.Biweekly, + Weekday: time.Friday, + Hour: 12, + NextRound: time.Now().Add(72 * time.Hour), + }) + + // Write member to the database + isActive := true + hasGenderPreference := true + + db.Create(&models.Member{ + ChannelID: channelID, + UserID: userID, + Gender: models.Male, + Country: sqlcrypter.NewEncryptedBytes("United States of America"), + City: sqlcrypter.NewEncryptedBytes("New York"), + IsActive: &isActive, + HasGenderPreference: &hasGenderPreference, + }) + + // Update member + isActive = false + hasGenderPreference = false p := &UpdateMemberParams{ ChannelID: channelID, UserID: userID, Gender: models.Male.String(), - IsActive: true, - HasGenderPreference: true, + City: sqlcrypter.NewEncryptedBytes("Los Angeles"), + IsActive: &isActive, + HasGenderPreference: &hasGenderPreference, } - err := UpdateMember(ctx, db, nil, p) + err = UpdateMember(ctx, db, nil, p) r.NoError(err) - r.NoError(mock.ExpectationsWereMet()) r.Contains(out.String(), "[INFO]") r.Contains(out.String(), "updated database row for the member") r.Contains(out.String(), fmt.Sprintf("slack_user_id=%s", userID)) + + // Verify changes + var member *models.Member + result := db.Model(&models.Member{}).Where("user_id = ?", userID).Where("channel_id = ?", channelID).First(&member) + r.NoError(result.Error) + r.Equal(result.RowsAffected, int64(1)) + r.False(*member.IsActive) + r.False(*member.HasGenderPreference) + r.Equal(member.City.String(), "Los Angeles") } func Test_ExecUpdateMember(t *testing.T) { @@ -74,7 +108,6 @@ func Test_ExecUpdateMember(t *testing.T) { userID, channelID, true, - false, database.AnyTime(), channelID, userID, @@ -82,10 +115,12 @@ func Test_ExecUpdateMember(t *testing.T) { WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() + isActive := true + p := &UpdateMemberParams{ ChannelID: channelID, UserID: userID, - IsActive: true, + IsActive: &isActive, } data, _ := json.Marshal(p) diff --git a/internal/bot/template.go b/internal/bot/template.go index 4ff842e..743089e 100644 --- a/internal/bot/template.go +++ b/internal/bot/template.go @@ -19,6 +19,7 @@ var ( funcMap = template.FuncMap{ "capitalize": templatex.Capitalize, "prettyDate": templatex.PrettierDate, + "prettyURL": templatex.PrettyURL, } templates = template.New("custom").Funcs(funcMap).Funcs(sprig.TxtFuncMap()) diff --git a/internal/bot/templates/app_home.json.tmpl b/internal/bot/templates/app_home.json.tmpl index 94e141e..9279b3f 100644 --- a/internal/bot/templates/app_home.json.tmpl +++ b/internal/bot/templates/app_home.json.tmpl @@ -39,7 +39,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette can be enabled on a Slack channel by inviting <@{{ .BotUserID }}> to it. The bot will pair up members of the Slack channel every round giving participants ample time to connect. Based on the channel's _Connection Mode_ setting, the bot will suggest connecting in person over coffee or virtually over a video call using Zoom, Google Meet, or Microsoft Teams." + "text": "Chat Roulette can be enabled on a Slack channel by inviting <@{{ .BotUserID }}> to it. The bot will pair up members of the Slack channel every round giving participants ample time to connect. Based on the channel's _Connection Mode_ setting, the bot will suggest connecting in person for :coffee: or virtually over :video_camera: using Zoom, Google Meet, or Microsoft Teams." } }, {{ if .Channels }} @@ -109,7 +109,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "_To view your Chat Roulette dashboard, click on the button to the right_" + "text": "_To view your personal Chat Roulette dashboard, click on the following button_" }, "accessory": { "type": "button", diff --git a/internal/bot/templates/check_pair.json.tmpl b/internal/bot/templates/check_pair.json.tmpl index 3c0076f..22348c6 100644 --- a/internal/bot/templates/check_pair.json.tmpl +++ b/internal/bot/templates/check_pair.json.tmpl @@ -4,7 +4,15 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@{{ .Participant }}> <@{{ .Partner }}>\n\nTime for an end of round check-in!", + "text": ":wave: Hi <@{{ .Participant }}> <@{{ .Partner }}>", + "verbatim": false + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Time for a check-in!", "verbatim": false } }, diff --git a/internal/bot/templates/check_pair_response.json.tmpl b/internal/bot/templates/check_pair_response.json.tmpl index 82b250b..2ee0081 100644 --- a/internal/bot/templates/check_pair_response.json.tmpl +++ b/internal/bot/templates/check_pair_response.json.tmpl @@ -4,7 +4,15 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@{{ .Participant }}> <@{{ .Partner }}>\n\nTime for an end of round check-in!", + "text": ":wave: Hi <@{{ .Participant }}> <@{{ .Partner }}>", + "verbatim": false + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Time for a check-in!", "verbatim": false } }, @@ -13,9 +21,9 @@ "text": { "type": "mrkdwn", {{ if .HasMet -}} - "text": ":white_check_mark: <@{{ .Responder }}> said that you met. Awesome!", + "text": ":white_check_mark: <@{{ .Responder }}> said that you met! That's awesome :tada:", {{ else -}} - "text": ":x: <@{{ .Responder }}> said that you did not meet. Sorry to hear that!", + "text": ":x: <@{{ .Responder }}> said that you did not meet. I'm really sorry to hear that :sob:", {{ end -}} "verbatim": false } diff --git a/internal/bot/templates/greet_admin.json.tmpl b/internal/bot/templates/greet_admin.json.tmpl index 7585995..deb5b8e 100644 --- a/internal/bot/templates/greet_admin.json.tmpl +++ b/internal/bot/templates/greet_admin.json.tmpl @@ -18,7 +18,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "I'm here to help your Slack community stay connected by introducing members of <#{{ .ChannelID }}> to each other on a regular cadence.", + "text": "I'm here to help your Slack community stay connected by introducing members of <#{{ .ChannelID }}> to each other on a regular cadence :smile:", "verbatim": false } }, @@ -37,13 +37,13 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "*Click the following button to enable Chat Roulette for this channel:*" + "text": "*Click on the following button to enable Chat Roulette for this channel:*" }, "accessory": { "type": "button", "text": { "type": "plain_text", - "text": "Let's Go!", + "text": ":rocket: Let's Go!", "emoji": true }, "value": "{{ .ChannelID }}", diff --git a/internal/bot/templates/greet_member.json.tmpl b/internal/bot/templates/greet_member.json.tmpl index 3329d55..939e0a6 100644 --- a/internal/bot/templates/greet_member.json.tmpl +++ b/internal/bot/templates/greet_member.json.tmpl @@ -11,14 +11,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "Welcome to the <#{{ .ChannelID }}> channel!" + "text": "Welcome to the <#{{ .ChannelID }}> channel :tada:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel. {{ .When }}, you will be introduced to another member in the <#{{ .ChannelID }}> channel. You will have until the end of each round to meet {{ if eq .ConnectionMode "virtual" }}virtually over a video call using Zoom, Google Meet, or Microsoft Teams{{ else if eq .ConnectionMode "physical" }}in person at a location of your choosing, whether it's for coffee or a meal{{ else if eq .ConnectionMode "hybrid" }}in person or virtually over a video call using Zoom, Google Meet, or Microsoft Teams{{ end }}!", + "text": "{{ .When }}, you will be introduced to another member in the <#{{ .ChannelID }}> channel. You will have until the end of each round to meet {{ if eq .ConnectionMode "virtual" }}virtually over :video_camera: using Zoom, Google Meet, or Microsoft Teams{{ else if eq .ConnectionMode "physical" }}in person at a location of your choosing, whether it's for :coffee: or :shallow_pan_of_food:{{ else if eq .ConnectionMode "hybrid" }}in person for :coffee: or virtually over :video_camera: using Zoom, Google Meet, or Microsoft Teams{{ end }}!", "verbatim": false } }, @@ -26,14 +26,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "The next Chat Roulette round begins on *{{ .NextRound | prettyDate }}*!" + "text": "The next Chat Roulette round begins on *{{ .NextRound | prettyDate }}* :smile:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel by <@{{ .Inviter }}>, so if you have any questions, please reach out to them." + "text": "Chat Roulette has been enabled on this channel by <@{{ .Inviter }}>, so if you have any questions, please reach out to them!" } }, { @@ -49,7 +49,7 @@ "type": "button", "text": { "type": "plain_text", - "text": "Opt In", + "text": ":white_check_mark: Opt In!", "emoji": true }, "value": "{{ .ChannelID }}", diff --git a/internal/bot/templates/notify_pair.json.tmpl b/internal/bot/templates/notify_pair.json.tmpl index 6ab0e80..e7f8bcb 100644 --- a/internal/bot/templates/notify_pair.json.tmpl +++ b/internal/bot/templates/notify_pair.json.tmpl @@ -4,49 +4,115 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@{{ .Participant.UserID }}> <@{{ .Partner.UserID }}>" + "text": ":wave: Hi <@{{ .Participant.UserID }}> <@{{ .Partner.UserID }}>" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "I'm here to help facilitate a little human connection by introducing everyone in <#{{ .ChannelID }}> {{ .Interval }}." + "text": "I'm here to help facilitate a little human connection by introducing everyone in <#{{ .ChannelID }}> *{{ .Interval }}*!" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "You two have been paired for this round of Chat Roulette. Now that you're here, why not schedule {{ if eq .ConnectionMode "virtual" }}a video call using Zoom, Google Meet, or Microsoft Teams to get to know each other{{ else if eq .ConnectionMode "physical" }}an in person meet up over coffee at a location that works for both of you{{ else if eq .ConnectionMode "hybrid" }}an in person meet up over coffee or a video call to get to know each other{{ end }}!" + "text": "You two have been paired up for this round of Chat Roulette :tada:" } }, { - "type": "header", + "type": "divider" + }, + { + "type": "section", "text": { - "type": "plain_text", - "text": "Match Info", - "emoji": true + "type": "mrkdwn", + "text": ":identification_card: *Name:* <@{{ .Participant.UserID }}>" } }, + {{- if ne .ConnectionMode "physical" }} { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":earth_americas: *Location*: {{ .Participant.City }}, {{ .Participant.Country }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":clock4: *Timezone*: {{ .ParticipantTimezone }}" + } + }, + {{- end }} + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *{{ .Participant.ProfileType }}:* {{ .Participant.ProfileLink.String | prettyURL }}" + } + } + {{- if .Participant.CalendlyLink }} + ,{ + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":spiral_calendar_pad: *Calendly:* {{ .Participant.CalendlyLink.String | prettyURL }}" + } + } + {{- end }} + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@{{ .Participant.UserID }}>\n*Location:* {{ .Participant.City }}, {{ .Participant.Country }}\n*Timezone*: {{ .ParticipantTimezone }}\n*{{ .Participant.ProfileType }}:* {{ .Participant.ProfileLink }}{{ if .Participant.CalendlyLink }}\n*Calendly*: {{ .Participant.CalendlyLink }}{{ end }}" + "text": ":identification_card: *Name:* <@{{ .Partner.UserID }}>" + } + }, + {{- if ne .ConnectionMode "physical" }} + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":earth_americas: *Location*: {{ .Partner.City }}, {{ .Partner.Country }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":clock4: *Timezone*: {{ .PartnerTimezone }}" } }, + {{- end }} { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *{{ .Partner.ProfileType }}:* {{ .Partner.ProfileLink.String | prettyURL }}" + } + } + {{- if .Partner.CalendlyLink }} + ,{ + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":spiral_calendar_pad: *Calendly:* {{ .Partner.CalendlyLink.String | prettyURL }}" + } + } + {{- end }} + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@{{ .Partner.UserID }}>\n*Location:* {{ .Partner.City }}, {{ .Partner.Country }}\n*Timezone*: {{ .PartnerTimezone }}\n*{{ .Partner.ProfileType }}:* {{ .Partner.ProfileLink }}{{ if .Partner.CalendlyLink }}\n*Calendly*: {{ .Partner.CalendlyLink }}{{ end }}" + "text": "Now that you're here, why don't we start with introductions! Then, schedule {{ if eq .ConnectionMode "virtual" }}a :video_camera: call using Zoom, Google Meet, or Microsoft Teams{{ else if eq .ConnectionMode "physical" }}an in person meet up over :coffee: or :shallow_pan_of_food: at a location that works for the both of you{{ else if eq .ConnectionMode "hybrid" }}an in person meet up over :coffee: or :shallow_pan_of_food: or a :video_camera: call{{ end }} to get acquainted!" } } ] diff --git a/internal/bot/templates/onboarding_channel.json.tmpl b/internal/bot/templates/onboarding_channel.json.tmpl index a598a89..d55db69 100644 --- a/internal/bot/templates/onboarding_channel.json.tmpl +++ b/internal/bot/templates/onboarding_channel.json.tmpl @@ -16,16 +16,21 @@ }, "submit": { "type": "plain_text", - "text": "Next", + "text": "Submit", "emoji": true }, "blocks": [ + { + "type": "image", + "image_url": "{{ .ImageURL }}", + "alt_text": "configuration" + }, { "type": "section", "block_id": "intro", "text": { "type": "mrkdwn", - "text": "The following settings will apply for channel <#{{ .ChannelID }}>" + "text": "The following settings will apply for <#{{ .ChannelID }}>" } }, { @@ -147,9 +152,14 @@ }, "label": { "type": "plain_text", - "text": "Select the date and time for the first round of Chat Roulette\n\nNote: the weekday and hour will be the same for every round!", + "text": "Select the date and time for the first round of Chat Roulette:", "emoji": true - } + }, + "hint": { + "type": "plain_text", + "text": "Note: the same weekday and hour will be used for every round!", + "emoji": true + }, } ] } diff --git a/internal/bot/templates/onboarding_gender.json.tmpl b/internal/bot/templates/onboarding_gender.json.tmpl index 5d868b7..8057580 100644 --- a/internal/bot/templates/onboarding_gender.json.tmpl +++ b/internal/bot/templates/onboarding_gender.json.tmpl @@ -20,6 +20,11 @@ "emoji": true }, "blocks": [ + { + "type": "image", + "image_url": "{{ .ImageURL }}", + "alt_text": "gender" + }, { "type": "section", "block_id": "onboarding", @@ -28,9 +33,6 @@ "text": "To allow participants to match with only others of the same gender, Chat Roulette for Slack needs to know your gender." } }, - { - "type": "divider" - }, { "type": "input", "block_id": "onboarding-gender-select", @@ -76,7 +78,7 @@ { "text": { "type": "plain_text", - "text": "I would like to opt-in to this feature", + "text": "I would like to opt-in to this feature!", "emoji": true }, "value": "true", diff --git a/internal/bot/testdata/app_home.json.golden b/internal/bot/testdata/app_home.json.golden index 7a2abe0..abe9369 100644 --- a/internal/bot/testdata/app_home.json.golden +++ b/internal/bot/testdata/app_home.json.golden @@ -39,7 +39,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette can be enabled on a Slack channel by inviting <@U0123456789> to it. The bot will pair up members of the Slack channel every round giving participants ample time to connect. Based on the channel's _Connection Mode_ setting, the bot will suggest connecting in person over coffee or virtually over a video call using Zoom, Google Meet, or Microsoft Teams." + "text": "Chat Roulette can be enabled on a Slack channel by inviting <@U0123456789> to it. The bot will pair up members of the Slack channel every round giving participants ample time to connect. Based on the channel's _Connection Mode_ setting, the bot will suggest connecting in person for :coffee: or virtually over :video_camera: using Zoom, Google Meet, or Microsoft Teams." } }, @@ -105,7 +105,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "_To view your Chat Roulette dashboard, click on the button to the right_" + "text": "_To view your personal Chat Roulette dashboard, click on the following button_" }, "accessory": { "type": "button", diff --git a/internal/bot/testdata/check_pair.json.golden b/internal/bot/testdata/check_pair.json.golden index ad19374..ca057c8 100644 --- a/internal/bot/testdata/check_pair.json.golden +++ b/internal/bot/testdata/check_pair.json.golden @@ -4,7 +4,15 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@U0123456789> <@U9876543210>\n\nTime for an end of round check-in!", + "text": ":wave: Hi <@U0123456789> <@U9876543210>", + "verbatim": false + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Time for a check-in!", "verbatim": false } }, diff --git a/internal/bot/testdata/check_pair_response.json.golden b/internal/bot/testdata/check_pair_response.json.golden index 15349d9..da28f13 100644 --- a/internal/bot/testdata/check_pair_response.json.golden +++ b/internal/bot/testdata/check_pair_response.json.golden @@ -4,7 +4,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@U0123456789> <@U9876543210>\n\nTime for an end of round check-in!", + "text": ":wave: Hi <@U0123456789> <@U9876543210>", "verbatim": false } }, @@ -12,7 +12,15 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":white_check_mark: <@U9876543210> said that you met. Awesome!", + "text": "Time for a check-in!", + "verbatim": false + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":white_check_mark: <@U9876543210> said that you met! That's awesome :tada:", "verbatim": false } } diff --git a/internal/bot/testdata/greet_admin.json.golden b/internal/bot/testdata/greet_admin.json.golden index 003f71b..64200a7 100644 --- a/internal/bot/testdata/greet_admin.json.golden +++ b/internal/bot/testdata/greet_admin.json.golden @@ -18,7 +18,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "I'm here to help your Slack community stay connected by introducing members of <#C0123456789> to each other on a regular cadence.", + "text": "I'm here to help your Slack community stay connected by introducing members of <#C0123456789> to each other on a regular cadence :smile:", "verbatim": false } }, @@ -37,13 +37,13 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "*Click the following button to enable Chat Roulette for this channel:*" + "text": "*Click on the following button to enable Chat Roulette for this channel:*" }, "accessory": { "type": "button", "text": { "type": "plain_text", - "text": "Let's Go!", + "text": ":rocket: Let's Go!", "emoji": true }, "value": "C0123456789", diff --git a/internal/bot/testdata/greet_member_biweekly.json.golden b/internal/bot/testdata/greet_member_biweekly.json.golden index 6cb4441..573b49f 100644 --- a/internal/bot/testdata/greet_member_biweekly.json.golden +++ b/internal/bot/testdata/greet_member_biweekly.json.golden @@ -11,14 +11,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "Welcome to the <#C0123456789> channel!" + "text": "Welcome to the <#C0123456789> channel :tada:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel. *Biweekly* on *Mondays*, you will be introduced to another member in the <#C0123456789> channel. You will have until the end of each round to meet in person at a location of your choosing, whether it's for coffee or a meal!", + "text": "*Biweekly* on *Mondays*, you will be introduced to another member in the <#C0123456789> channel. You will have until the end of each round to meet in person at a location of your choosing, whether it's for :coffee: or :shallow_pan_of_food:!", "verbatim": false } }, @@ -26,14 +26,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "The next Chat Roulette round begins on *Monday, January 3rd, 2022*!" + "text": "The next Chat Roulette round begins on *Monday, January 3rd, 2022* :smile:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel by <@U9876543210>, so if you have any questions, please reach out to them." + "text": "Chat Roulette has been enabled on this channel by <@U9876543210>, so if you have any questions, please reach out to them!" } }, { @@ -49,7 +49,7 @@ "type": "button", "text": { "type": "plain_text", - "text": "Opt In", + "text": ":white_check_mark: Opt In!", "emoji": true }, "value": "C0123456789", diff --git a/internal/bot/testdata/greet_member_hybrid.json.golden b/internal/bot/testdata/greet_member_hybrid.json.golden index bd32498..aa14b8a 100644 --- a/internal/bot/testdata/greet_member_hybrid.json.golden +++ b/internal/bot/testdata/greet_member_hybrid.json.golden @@ -11,14 +11,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "Welcome to the <#C0123456789> channel!" + "text": "Welcome to the <#C0123456789> channel :tada:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel. *Weekly* on *Mondays*, you will be introduced to another member in the <#C0123456789> channel. You will have until the end of each round to meet in person or virtually over a video call using Zoom, Google Meet, or Microsoft Teams!", + "text": "*Weekly* on *Mondays*, you will be introduced to another member in the <#C0123456789> channel. You will have until the end of each round to meet in person for :coffee: or virtually over :video_camera: using Zoom, Google Meet, or Microsoft Teams!", "verbatim": false } }, @@ -26,14 +26,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "The next Chat Roulette round begins on *Monday, January 3rd, 2022*!" + "text": "The next Chat Roulette round begins on *Monday, January 3rd, 2022* :smile:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel by <@U9876543210>, so if you have any questions, please reach out to them." + "text": "Chat Roulette has been enabled on this channel by <@U9876543210>, so if you have any questions, please reach out to them!" } }, { @@ -49,7 +49,7 @@ "type": "button", "text": { "type": "plain_text", - "text": "Opt In", + "text": ":white_check_mark: Opt In!", "emoji": true }, "value": "C0123456789", diff --git a/internal/bot/testdata/greet_member_monthly.json.golden b/internal/bot/testdata/greet_member_monthly.json.golden index 15ec622..11e3ce9 100644 --- a/internal/bot/testdata/greet_member_monthly.json.golden +++ b/internal/bot/testdata/greet_member_monthly.json.golden @@ -11,14 +11,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "Welcome to the <#C0123456789> channel!" + "text": "Welcome to the <#C0123456789> channel :tada:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel. On the *first Monday* of every month, you will be introduced to another member in the <#C0123456789> channel. You will have until the end of each round to meet virtually over a video call using Zoom, Google Meet, or Microsoft Teams!", + "text": "On the *first Monday* of every month, you will be introduced to another member in the <#C0123456789> channel. You will have until the end of each round to meet virtually over :video_camera: using Zoom, Google Meet, or Microsoft Teams!", "verbatim": false } }, @@ -26,14 +26,14 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "The next Chat Roulette round begins on *Monday, January 3rd, 2022*!" + "text": "The next Chat Roulette round begins on *Monday, January 3rd, 2022* :smile:" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "Chat Roulette has been enabled on this channel by <@U9876543210>, so if you have any questions, please reach out to them." + "text": "Chat Roulette has been enabled on this channel by <@U9876543210>, so if you have any questions, please reach out to them!" } }, { @@ -49,7 +49,7 @@ "type": "button", "text": { "type": "plain_text", - "text": "Opt In", + "text": ":white_check_mark: Opt In!", "emoji": true }, "value": "C0123456789", diff --git a/internal/bot/testdata/notify_pair_hybrid.json.golden b/internal/bot/testdata/notify_pair_hybrid.json.golden index f1fa57f..d3eaf98 100644 --- a/internal/bot/testdata/notify_pair_hybrid.json.golden +++ b/internal/bot/testdata/notify_pair_hybrid.json.golden @@ -4,49 +4,100 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@U0123456789> <@U9876543210>" + "text": ":wave: Hi <@U0123456789> <@U9876543210>" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "I'm here to help facilitate a little human connection by introducing everyone in <#C0123456789> biweekly." + "text": "I'm here to help facilitate a little human connection by introducing everyone in <#C0123456789> *biweekly*!" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "You two have been paired for this round of Chat Roulette. Now that you're here, why not schedule an in person meet up over coffee or a video call to get to know each other!" + "text": "You two have been paired up for this round of Chat Roulette :tada:" } }, { - "type": "header", + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":identification_card: *Name:* <@U0123456789>" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":earth_americas: *Location*: Nairobi, Kenya" + } + }, + { + "type": "section", "text": { - "type": "plain_text", - "text": "Match Info", - "emoji": true + "type": "mrkdwn", + "text": ":clock4: *Timezone*: EAT (UTC+03:00)" } }, { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *GitHub:* github.com/AhmedARmohamed" + } + } + ,{ + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":spiral_calendar_pad: *Calendly:* calendly.com/AhmedARmohamed" + } + } + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@U0123456789>\n*Location:* Nairobi, Kenya\n*Timezone*: EAT (UTC+03:00)\n*Github:* https://github.com/AhmedARmohamed\n*Calendly*: https://calendly.com/AhmedARmohamed" + "text": ":identification_card: *Name:* <@U9876543210>" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":earth_americas: *Location*: Phoenix, United States" } }, { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":clock4: *Timezone*: MST (UTC-07:00)" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *GitHub:* github.com/bincyber" + } + } + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@U9876543210>\n*Location:* Phoenix, United States\n*Timezone*: MST (UTC-07:00)\n*Github:* https://github.com/bincyber" + "text": "Now that you're here, why don't we start with introductions! Then, schedule an in person meet up over :coffee: or :shallow_pan_of_food: or a :video_camera: call to get acquainted!" } } ] diff --git a/internal/bot/testdata/notify_pair_physical.json.golden b/internal/bot/testdata/notify_pair_physical.json.golden index fb69972..7abeb3c 100644 --- a/internal/bot/testdata/notify_pair_physical.json.golden +++ b/internal/bot/testdata/notify_pair_physical.json.golden @@ -4,49 +4,72 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@U0123456789> <@U9876543210>" + "text": ":wave: Hi <@U0123456789> <@U9876543210>" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "I'm here to help facilitate a little human connection by introducing everyone in <#C0123456789> biweekly." + "text": "I'm here to help facilitate a little human connection by introducing everyone in <#C0123456789> *biweekly*!" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "You two have been paired for this round of Chat Roulette. Now that you're here, why not schedule an in person meet up over coffee at a location that works for both of you!" + "text": "You two have been paired up for this round of Chat Roulette :tada:" } }, { - "type": "header", + "type": "divider" + }, + { + "type": "section", "text": { - "type": "plain_text", - "text": "Match Info", - "emoji": true + "type": "mrkdwn", + "text": ":identification_card: *Name:* <@U0123456789>" } }, { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *GitHub:* github.com/AhmedARmohamed" + } + } + ,{ + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":spiral_calendar_pad: *Calendly:* calendly.com/AhmedARmohamed" + } + } + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@U0123456789>\n*Location:* Nairobi, Kenya\n*Timezone*: EAT (UTC+03:00)\n*Github:* https://github.com/AhmedARmohamed\n*Calendly*: https://calendly.com/AhmedARmohamed" + "text": ":identification_card: *Name:* <@U9876543210>" } }, { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *GitHub:* github.com/bincyber" + } + } + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@U9876543210>\n*Location:* Phoenix, United States\n*Timezone*: MST (UTC-07:00)\n*Github:* https://github.com/bincyber" + "text": "Now that you're here, why don't we start with introductions! Then, schedule an in person meet up over :coffee: or :shallow_pan_of_food: at a location that works for the both of you to get acquainted!" } } ] diff --git a/internal/bot/testdata/notify_pair_virtual.json.golden b/internal/bot/testdata/notify_pair_virtual.json.golden index 85871d0..5d84d02 100644 --- a/internal/bot/testdata/notify_pair_virtual.json.golden +++ b/internal/bot/testdata/notify_pair_virtual.json.golden @@ -4,49 +4,100 @@ "type": "section", "text": { "type": "mrkdwn", - "text": ":wave: <@U0123456789> <@U9876543210>" + "text": ":wave: Hi <@U0123456789> <@U9876543210>" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "I'm here to help facilitate a little human connection by introducing everyone in <#C0123456789> biweekly." + "text": "I'm here to help facilitate a little human connection by introducing everyone in <#C0123456789> *biweekly*!" } }, { "type": "section", "text": { "type": "mrkdwn", - "text": "You two have been paired for this round of Chat Roulette. Now that you're here, why not schedule a video call using Zoom, Google Meet, or Microsoft Teams to get to know each other!" + "text": "You two have been paired up for this round of Chat Roulette :tada:" } }, { - "type": "header", + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":identification_card: *Name:* <@U0123456789>" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":earth_americas: *Location*: Nairobi, Kenya" + } + }, + { + "type": "section", "text": { - "type": "plain_text", - "text": "Match Info", - "emoji": true + "type": "mrkdwn", + "text": ":clock4: *Timezone*: EAT (UTC+03:00)" } }, { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *GitHub:* github.com/AhmedARmohamed" + } + } + ,{ + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":spiral_calendar_pad: *Calendly:* calendly.com/AhmedARmohamed" + } + } + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@U0123456789>\n*Location:* Nairobi, Kenya\n*Timezone*: EAT (UTC+03:00)\n*Github:* https://github.com/AhmedARmohamed\n*Calendly*: https://calendly.com/AhmedARmohamed" + "text": ":identification_card: *Name:* <@U9876543210>" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":earth_americas: *Location*: Phoenix, United States" } }, { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":clock4: *Timezone*: MST (UTC-07:00)" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":sparkles: *GitHub:* github.com/bincyber" + } + } + ,{ "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", - "text": "*Name:* <@U9876543210>\n*Location:* Phoenix, United States\n*Timezone*: MST (UTC-07:00)\n*Github:* https://github.com/bincyber" + "text": "Now that you're here, why don't we start with introductions! Then, schedule a :video_camera: call using Zoom, Google Meet, or Microsoft Teams to get acquainted!" } } ] diff --git a/internal/bot/testdata/onboarding_channel.json.golden b/internal/bot/testdata/onboarding_channel.json.golden index 0aa67e6..99c150e 100644 --- a/internal/bot/testdata/onboarding_channel.json.golden +++ b/internal/bot/testdata/onboarding_channel.json.golden @@ -17,16 +17,21 @@ b3N0L2FjdGlvbnMvYS9iL2MifQ==", }, "submit": { "type": "plain_text", - "text": "Next", + "text": "Submit", "emoji": true }, "blocks": [ + { + "type": "image", + "image_url": "http://localhost/static/img/coffee-machine.jpg", + "alt_text": "configuration" + }, { "type": "section", "block_id": "intro", "text": { "type": "mrkdwn", - "text": "The following settings will apply for channel <#C0123456789>" + "text": "The following settings will apply for <#C0123456789>" } }, { @@ -148,9 +153,14 @@ b3N0L2FjdGlvbnMvYS9iL2MifQ==", }, "label": { "type": "plain_text", - "text": "Select the date and time for the first round of Chat Roulette\n\nNote: the weekday and hour will be the same for every round!", + "text": "Select the date and time for the first round of Chat Roulette:", "emoji": true - } + }, + "hint": { + "type": "plain_text", + "text": "Note: the same weekday and hour will be used for every round!", + "emoji": true + }, } ] } diff --git a/internal/bot/testdata/onboarding_gender.json.golden b/internal/bot/testdata/onboarding_gender.json.golden index 5654da6..e46d1ae 100644 --- a/internal/bot/testdata/onboarding_gender.json.golden +++ b/internal/bot/testdata/onboarding_gender.json.golden @@ -20,6 +20,11 @@ "emoji": true }, "blocks": [ + { + "type": "image", + "image_url": "http://localhost/static/img/gender.jpg", + "alt_text": "gender" + }, { "type": "section", "block_id": "onboarding", @@ -28,9 +33,6 @@ "text": "To allow participants to match with only others of the same gender, Chat Roulette for Slack needs to know your gender." } }, - { - "type": "divider" - }, { "type": "input", "block_id": "onboarding-gender-select", @@ -76,7 +78,7 @@ { "text": { "type": "plain_text", - "text": "I would like to opt-in to this feature", + "text": "I would like to opt-in to this feature!", "emoji": true }, "value": "true", diff --git a/internal/database/migrations/20221112215807_init.up.sql b/internal/database/migrations/20221112215807_init.up.sql index b48af0e..377d293 100644 --- a/internal/database/migrations/20221112215807_init.up.sql +++ b/internal/database/migrations/20221112215807_init.up.sql @@ -16,7 +16,6 @@ CREATE TABLE IF NOT EXISTS channels ( created_at timestamp without time zone DEFAULT NOW()::timestamp NOT NULL, updated_at timestamp without time zone DEFAULT NOW()::timestamp NOT NULL, -- 'connection_mode', -- added in v1.2.0 - -- is_active boolean DEFAULT false NOT NULL, -- added in v1.3.0 CONSTRAINT channels_pk_channel_id PRIMARY KEY (channel_id) ); diff --git a/internal/database/migrations/20240825130851_gender.up.sql b/internal/database/migrations/20240825130851_gender.up.sql index 27edf2d..543f84c 100644 --- a/internal/database/migrations/20240825130851_gender.up.sql +++ b/internal/database/migrations/20240825130851_gender.up.sql @@ -12,7 +12,8 @@ ALTER TABLE members ALTER COLUMN has_gender_preference SET NOT NULL; -- GetRandomMatchesV2() retrieves a randomized set of matches for a round of Chat Roulette -- while respecting those members' who prefer to be matched with the same gender (has_gender_preference = true). -- To maximize the number of matches, members who prefer being matched with the same gender are matched first --- before others. Any participants who could not be matched are returned with the "partner" column set to null. +-- before others. If no match with gender preference is found, it tries to match with users without preference. +-- Any participants who could not be matched are returned with the "partner" column set to null. -- -- Example Usage: -- SELECT * FROM GetRandomMatchesV2('C122315531'); @@ -28,21 +29,24 @@ BEGIN FOR v_user IN ( SELECT user_id, gender FROM members - WHERE channel_id = p_channel_id AND is_active AND has_gender_preference + WHERE channel_id = p_channel_id + AND is_active + AND has_gender_preference ORDER BY RANDOM() ) LOOP IF v_user.user_id = ANY(v_matched_users) THEN CONTINUE; END IF; + -- Try to match with a user who has gender preference SELECT user_id INTO v_partner FROM members - WHERE channel_id = p_channel_id - AND is_active - AND has_gender_preference - AND gender = v_user.gender - AND user_id != v_user.user_id - AND user_id != ALL(v_matched_users) + WHERE channel_id = p_channel_id + AND is_active + AND gender = v_user.gender + AND user_id != v_user.user_id + AND user_id != ALL(v_matched_users) + ORDER BY has_gender_preference DESC LIMIT 1; IF v_partner IS NOT NULL THEN @@ -50,11 +54,11 @@ BEGIN RETURN QUERY SELECT v_user.user_id::VARCHAR, v_partner::VARCHAR; ELSE v_matched_users := v_matched_users || v_user.user_id; - RETURN QUERY SELECT v_user.user_id::VARCHAR, NULL::VARCHAR; + RETURN QUERY SELECT v_user.user_id::VARCHAR, NULL::VARCHAR; END IF; END LOOP; - -- Match remaining users + -- Match remaining users who dont have gender preference FOR v_user IN ( SELECT user_id FROM members @@ -67,10 +71,10 @@ BEGIN SELECT user_id INTO v_partner FROM members - WHERE channel_id = p_channel_id - AND is_active - AND user_id != v_user.user_id - AND user_id != ALL(v_matched_users) + WHERE channel_id = p_channel_id + AND is_active + AND user_id != v_user.user_id + AND user_id != ALL(v_matched_users) LIMIT 1; IF v_partner IS NOT NULL THEN diff --git a/internal/isx/is.go b/internal/isx/is.go index 9ad8efc..d6f81e4 100644 --- a/internal/isx/is.go +++ b/internal/isx/is.go @@ -133,11 +133,10 @@ func CalendlyLink(value interface{}) error { s, _ := value.(string) if s != "" { - regex := regexp.MustCompile(`(?:https:\/\/)?calendly.com\/\S+`) + regex := regexp.MustCompile(`(?i)(?:https:\/\/)?calendly.com\/\S+`) if err := validation.Validate(s, validation.Required, - is.URL, validation.Match(regex).Error("URL is malformed"), ); err != nil { return fmt.Errorf("invalid Calendly link") diff --git a/internal/isx/is_test.go b/internal/isx/is_test.go index 6df595d..49701ef 100644 --- a/internal/isx/is_test.go +++ b/internal/isx/is_test.go @@ -85,6 +85,7 @@ func Test_CalendlyLink(t *testing.T) { {"empty", "", false}, {"invalid domain", "twitter.com/bincyber", true}, {"no user", "calendly.com/", true}, + {"ignore case", "HTTPS://Calendly.com/bincyber", false}, } for _, tc := range tt { diff --git a/internal/server/api/v1/interactions.go b/internal/server/api/v1/interactions.go index bee4b42..bb0920c 100644 --- a/internal/server/api/v1/interactions.go +++ b/internal/server/api/v1/interactions.go @@ -93,7 +93,7 @@ func (s *implServer) slackInteractionHandler(w http.ResponseWriter, r *http.Requ switch interaction.View.CallbackID { case "onboarding-admin-modal": // Respond to the HTTP request with the new view - body, err := bot.RenderOnboardingChannelView(r.Context(), &interaction) + body, err := bot.RenderOnboardingChannelView(r.Context(), &interaction, s.GetBaseURL()) if err != nil { span.RecordError(err) logger.Error("failed to load onboarding channel template", "error", err) @@ -262,7 +262,7 @@ func (s *implServer) slackInteractionHandler(w http.ResponseWriter, r *http.Requ calendlyLink := strings.ToLower(interaction.View.State.Values["onboarding-calendly"]["onboarding-calendly"].Value) if calendlyLink != "" { - // Validate the Slack user's calendly link, if provided + // Validate the Slack user's calendly link if err := bot.ValidateMemberCalendlyLink(r.Context(), calendlyLink); err != nil { span.RecordError(err) @@ -292,6 +292,14 @@ func (s *implServer) slackInteractionHandler(w http.ResponseWriter, r *http.Requ } } + // Mark the user as active since they have completed onboarding + if err := bot.SetMemberIsActive(r.Context(), s.GetDB(), &interaction); err != nil { + span.RecordError(err) + logger.Error("failed to upsert Slack member's profile info", "error", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + // Update the original GREET_MEMBER message to remove the Opt-In button if err := bot.RespondGreetMemberWebhook(r.Context(), s.GetHTTPClient(), &interaction); err != nil { span.RecordError(err) diff --git a/internal/server/api/v1/member.go b/internal/server/api/v1/member.go index 1c033f2..8631de6 100644 --- a/internal/server/api/v1/member.go +++ b/internal/server/api/v1/member.go @@ -16,9 +16,9 @@ import ( "github.com/chat-roulettte/chat-roulette/internal/bot" "github.com/chat-roulettte/chat-roulette/internal/database/models" "github.com/chat-roulettte/chat-roulette/internal/isx" + "github.com/chat-roulettte/chat-roulette/internal/templatex" ) -// TODO: convert received json to bot.UpdateMemberParams struct type updateMemberRequest struct { ChannelID string `json:"channel_id"` UserID string `json:"user_id"` @@ -28,8 +28,8 @@ type updateMemberRequest struct { ProfileType string `json:"profile_type,omitempty"` ProfileLink string `json:"profile_link,omitempty"` CalendlyLink string `json:"calendly_link,omitempty"` - IsActive bool `json:"is_active"` - HasGenderPreference bool `json:"has_gender_preference"` + IsActive *bool `json:"is_active,omitempty"` + HasGenderPreference *bool `json:"has_gender_preference,omitempty"` } // updateMemberHandler handles updating a member's profile settings @@ -132,7 +132,11 @@ func (s *implServer) updateMemberHandler(w http.ResponseWriter, r *http.Request) } if req.City != "" { - p.City = sqlcrypter.NewEncryptedBytes(req.City) + p.City = sqlcrypter.NewEncryptedBytes( + strings.TrimSpace( + templatex.Capitalize(req.City), + ), + ) } if req.Timezone != "" { @@ -144,11 +148,18 @@ func (s *implServer) updateMemberHandler(w http.ResponseWriter, r *http.Request) } if req.ProfileLink != "" { - p.ProfileLink = sqlcrypter.NewEncryptedBytes(req.ProfileLink) + p.ProfileLink = sqlcrypter.NewEncryptedBytes( + strings.TrimSpace( + strings.ToLower(req.ProfileLink), + ), + ) } if req.CalendlyLink != "" { - p.CalendlyLink = sqlcrypter.NewEncryptedBytes(req.CalendlyLink) + p.CalendlyLink = sqlcrypter.NewEncryptedBytes( + strings.TrimSpace( + strings.ToLower(req.CalendlyLink), + )) } // Schedule an UPDATE_MEMBER job to update the member's participation status diff --git a/internal/server/api/v1/member_test.go b/internal/server/api/v1/member_test.go index 34d3a7a..964987a 100644 --- a/internal/server/api/v1/member_test.go +++ b/internal/server/api/v1/member_test.go @@ -43,10 +43,12 @@ func Test_updateMemberHandler(t *testing.T) { router := mux.NewRouter() router.HandleFunc(path, s.updateMemberHandler).Methods(method) + isActive := false + params := updateMemberRequest{ ChannelID: "C9876543210", UserID: "U0123456789", - IsActive: true, + IsActive: &isActive, Country: "United States of America", City: "Phoenix", Timezone: "America/Phoenix", @@ -89,10 +91,12 @@ func Test_updateMemberHandler(t *testing.T) { t.Run("validation", func(t *testing.T) { r := require.New(t) + isActive := true + p := &bot.UpdateMemberParams{ ChannelID: "C9876543210", UserID: "U0123456789", - IsActive: true, + IsActive: &isActive, } body := new(bytes.Buffer) diff --git a/internal/server/server.go b/internal/server/server.go index 46a70c4..9add23f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -234,7 +234,7 @@ func (s *Server) GetSlackBotUserID() string { // GetBaseURL returns the base URL of the server func (s *Server) GetBaseURL() string { u, _ := url.Parse(s.config.Server.RedirectURL) - u.Path = "/" + u.Path = "" return u.String() } diff --git a/internal/server/ui/history.go b/internal/server/ui/history.go index ded14fe..4893311 100644 --- a/internal/server/ui/history.go +++ b/internal/server/ui/history.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "net/http" + "net/url" "sync" "time" @@ -172,6 +173,10 @@ func (s *implServer) historyHandler(w http.ResponseWriter, r *http.Request) { history[i].Image = match.Profile.Image192 } + // Ensure social links include the scheme + u, _ := url.Parse(history[i].Social.String()) // We already know it's a valid URL + u.Scheme = "https" + history[i].Social = sqlcrypter.NewEncryptedBytes(u.String()) }(i, e) } diff --git a/internal/server/ui/static/img/coffee-machine.jpg b/internal/server/ui/static/img/coffee-machine.jpg new file mode 100644 index 0000000..3e812fe Binary files /dev/null and b/internal/server/ui/static/img/coffee-machine.jpg differ diff --git a/internal/server/ui/static/img/gender.jpg b/internal/server/ui/static/img/gender.jpg new file mode 100644 index 0000000..1dc3cd5 Binary files /dev/null and b/internal/server/ui/static/img/gender.jpg differ diff --git a/internal/server/ui/template.go b/internal/server/ui/template.go index a3079f2..3f824de 100644 --- a/internal/server/ui/template.go +++ b/internal/server/ui/template.go @@ -30,6 +30,7 @@ var ( "capitalize": templatex.Capitalize, "capitalizeInterval": templatex.CapitalizeInterval, "prettyDate": templatex.PrettyDate, + "prettyURL": templatex.PrettyURL, "derefBool": templatex.DerefBool, }, sprig.HtmlFuncMap(), diff --git a/internal/server/ui/templates/member.gohtml b/internal/server/ui/templates/member.gohtml index a0344e7..1c0a330 100644 --- a/internal/server/ui/templates/member.gohtml +++ b/internal/server/ui/templates/member.gohtml @@ -76,11 +76,13 @@ -

Enabling this setting will prevent you from being matched in future rounds of chat-roulette

+

Enabling this setting will prevent you from being matched in + future rounds of chat-roulette

-