-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bot will now notify participants who did not get matched
- Loading branch information
Showing
12 changed files
with
392 additions
and
3 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package bot | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"time" | ||
|
||
"github.com/hashicorp/go-hclog" | ||
"github.com/pkg/errors" | ||
"github.com/slack-go/slack" | ||
"gorm.io/gorm" | ||
|
||
"github.com/chat-roulettte/chat-roulette/internal/database/models" | ||
"github.com/chat-roulettte/chat-roulette/internal/o11y/attributes" | ||
) | ||
|
||
const ( | ||
notifyMemberTemplateFilename = "notify_member.json.tmpl" | ||
) | ||
|
||
// notifyMemberTemplate is used with templates/notify_member.json.tmpl | ||
type notifyMemberTemplate struct { | ||
ChannelID string | ||
UserID string | ||
NextRound time.Time | ||
} | ||
|
||
// NotifyMemberParams are the parameters for the NOTIFY_MEMBER job. | ||
type NotifyMemberParams struct { | ||
ChannelID string `json:"channel_id"` | ||
UserID string `json:"user_id"` | ||
} | ||
|
||
// NotifyMember sends an apologetic message to a participant for not being able to be matched in this round of chat roulette. | ||
func NotifyMember(ctx context.Context, db *gorm.DB, client *slack.Client, p *NotifyMemberParams) error { | ||
|
||
logger := hclog.FromContext(ctx).With( | ||
attributes.SlackChannelID, p.ChannelID, | ||
attributes.SlackUserID, p.UserID, | ||
) | ||
|
||
// Retrieve channel metadata from the database | ||
dbCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) | ||
defer cancel() | ||
|
||
var channel models.Channel | ||
|
||
if err := db.WithContext(dbCtx).Where("channel_id = ?", p.ChannelID).First(&channel).Error; err != nil { | ||
message := "failed to retrieve metadata for the Slack channel" | ||
logger.Error(message, "error", err) | ||
return errors.Wrap(err, message) | ||
} | ||
|
||
// Render template | ||
t := notifyMemberTemplate{ | ||
ChannelID: p.ChannelID, | ||
UserID: p.UserID, | ||
NextRound: channel.NextRound, | ||
} | ||
|
||
content, err := renderTemplate(notifyMemberTemplateFilename, t) | ||
if err != nil { | ||
return errors.Wrap(err, "failed to render template") | ||
} | ||
|
||
logger.Info("notifying Slack member with a message") | ||
|
||
// We can marshal the json 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 unmarshal JSON") | ||
} | ||
|
||
// Open a Slack DM with the user | ||
childCtx, cancel := context.WithTimeout(ctx, 3000*time.Millisecond) | ||
defer cancel() | ||
|
||
response, _, _, err := client.OpenConversationContext( | ||
childCtx, | ||
&slack.OpenConversationParameters{ | ||
ReturnIM: false, | ||
Users: []string{ | ||
p.UserID, | ||
}, | ||
}) | ||
|
||
if err != nil { | ||
logger.Error("failed to open Slack DM", "error", err) | ||
return err | ||
} | ||
|
||
// Send the Slack DM to the user | ||
if _, _, err = client.PostMessageContext( | ||
ctx, | ||
response.Conversation.ID, | ||
slack.MsgOptionBlocks(view.Blocks.BlockSet...), | ||
); err != nil { | ||
logger.Error("failed to send Slack direct message", "error", err) | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// QueueNotifyMemberJob adds a new NOTIFY_MEMBER job to the queue. | ||
func QueueNotifyMemberJob(ctx context.Context, db *gorm.DB, p *NotifyMemberParams) error { | ||
job := models.GenericJob[*NotifyMemberParams]{ | ||
JobType: models.JobTypeNotifyMember, | ||
Priority: models.JobPriorityStandard, | ||
Params: p, | ||
} | ||
|
||
return QueueJob(ctx, db, job) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package bot | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"database/sql/driver" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
|
||
"github.com/DATA-DOG/go-sqlmock" | ||
"github.com/hashicorp/go-hclog" | ||
"github.com/sebdah/goldie/v2" | ||
"github.com/slack-go/slack" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"github.com/stretchr/testify/suite" | ||
"gorm.io/gorm" | ||
|
||
"github.com/chat-roulettte/chat-roulette/internal/database" | ||
"github.com/chat-roulettte/chat-roulette/internal/database/models" | ||
"github.com/chat-roulettte/chat-roulette/internal/o11y" | ||
) | ||
|
||
type NotifyMemberSuite struct { | ||
suite.Suite | ||
ctx context.Context | ||
mock sqlmock.Sqlmock | ||
db *gorm.DB | ||
logger hclog.Logger | ||
buffer *bytes.Buffer | ||
} | ||
|
||
func (s *NotifyMemberSuite) SetupTest() { | ||
s.logger, s.buffer = o11y.NewBufferedLogger() | ||
s.ctx = hclog.WithContext(context.Background(), s.logger) | ||
s.db, s.mock = database.NewMockedGormDB() | ||
} | ||
|
||
func (s *NotifyMemberSuite) AfterTest(_, _ string) { | ||
require.NoError(s.T(), s.mock.ExpectationsWereMet()) | ||
} | ||
|
||
func (s *NotifyMemberSuite) Test_NotifyMember() { | ||
r := require.New(s.T()) | ||
|
||
p := &NotifyMemberParams{ | ||
ChannelID: "C9876543210", | ||
UserID: "U0123456789", | ||
} | ||
|
||
columns := []string{ | ||
"channel_id", | ||
"inviter", | ||
"interval", | ||
"weekday", | ||
"hour", | ||
"next_round", | ||
} | ||
|
||
row := []driver.Value{ | ||
p.ChannelID, | ||
"U8967452301", | ||
models.Weekly, | ||
time.Sunday, | ||
12, | ||
time.Now(), | ||
} | ||
|
||
// Mock retrieving channel metadata | ||
s.mock.ExpectQuery(`SELECT \* FROM "channels" WHERE channel_id = (.+) ORDER BY`). | ||
WithArgs( | ||
p.ChannelID, | ||
1, | ||
). | ||
WillReturnRows(sqlmock.NewRows(columns).AddRow(row...)) | ||
|
||
// Mock Slack API calls | ||
mux := http.NewServeMux() | ||
|
||
mux.HandleFunc("/conversations.open", func(w http.ResponseWriter, req *http.Request) { | ||
w.Write([]byte(`{"ok":true,"channel":{"id":"D1111111111"}}`)) | ||
}) | ||
|
||
mux.HandleFunc("/chat.postMessage", func(w http.ResponseWriter, req *http.Request) { | ||
req.ParseForm() | ||
|
||
b := req.FormValue("blocks") | ||
|
||
if b == "" { | ||
w.WriteHeader(http.StatusBadRequest) | ||
return | ||
} | ||
|
||
var blocks slack.Blocks | ||
if err := json.Unmarshal([]byte(b), &blocks); err != nil { | ||
w.WriteHeader(http.StatusBadRequest) | ||
w.Write([]byte(`{"ok":false}`)) | ||
} | ||
|
||
r.Len(blocks.BlockSet, 4) | ||
|
||
w.Write([]byte(`{ | ||
"ok": true, | ||
"channel": "D1111111111" | ||
}`)) | ||
}) | ||
|
||
httpServer := httptest.NewServer(mux) | ||
defer httpServer.Close() | ||
|
||
url := fmt.Sprintf("%s/", httpServer.URL) | ||
client := slack.New("xoxb-test-token-here", slack.OptionAPIURL(url)) | ||
|
||
err := NotifyMember(s.ctx, s.db, client, p) | ||
r.NoError(err) | ||
} | ||
|
||
func (s *NotifyMemberSuite) Test_NotifyMemberJob() { | ||
r := require.New(s.T()) | ||
|
||
p := &NotifyMemberParams{ | ||
ChannelID: "C0123456789", | ||
UserID: "U1111111111", | ||
} | ||
|
||
database.MockQueueJob( | ||
s.mock, | ||
p, | ||
models.JobTypeNotifyMember.String(), | ||
models.JobPriorityStandard, | ||
) | ||
|
||
err := QueueNotifyMemberJob(s.ctx, s.db, p) | ||
r.NoError(err) | ||
r.Contains(s.buffer.String(), "added new job to the database") | ||
} | ||
|
||
func Test_NotifyMember_suite(t *testing.T) { | ||
suite.Run(t, new(NotifyMemberSuite)) | ||
} | ||
|
||
func Test_notifyMemberTemplate(t *testing.T) { | ||
g := goldie.New(t) | ||
|
||
nextRound := time.Date(2022, time.January, 3, 12, 0, 0, 0, time.UTC) | ||
|
||
p := notifyMemberTemplate{ | ||
ChannelID: "C0123456789", | ||
UserID: "U0123456789", | ||
NextRound: nextRound, | ||
} | ||
|
||
content, err := renderTemplate(notifyMemberTemplateFilename, p) | ||
assert.Nil(t, err) | ||
|
||
g.Assert(t, "notify_member.json", []byte(content)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"blocks": [ | ||
{ | ||
"type": "section", | ||
"text": { | ||
"type": "mrkdwn", | ||
"text": "Hi <@{{ .UserID }}> :wave:" | ||
} | ||
}, | ||
{ | ||
"type": "section", | ||
"text": { | ||
"type": "mrkdwn", | ||
"text": "A new round of Chat Roulette has started for <#{{ .ChannelID }}>" | ||
} | ||
}, | ||
{ | ||
"type": "section", | ||
"text": { | ||
"type": "mrkdwn", | ||
"text": "I'm really sorry, I was unable to match you with someone this time :cry:" | ||
} | ||
}, | ||
{ | ||
"type": "section", | ||
"text": { | ||
"type": "mrkdwn", | ||
"text": "The next Chat Roulette round will be on *{{ .NextRound | prettyDate }}*. I'm hopeful that I will be able to connect you with someone then! :sweat_smile:" | ||
} | ||
} | ||
] | ||
} |
Oops, something went wrong.