Skip to content

Commit

Permalink
Merge pull request #17 from moneyforward/develop
Browse files Browse the repository at this point in the history
d2m: Support for sending more than 30 email addresses
  • Loading branch information
sho0126hiro authored Feb 28, 2023
2 parents 8b6b599 + dd3bc0a commit cb23816
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 22 deletions.
22 changes: 21 additions & 1 deletion app/internal/domain/service/slack_reaction_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,33 @@ func NewSlackReactionUsersService(factory repository2.Factory) *slackReactionUse
}
}

const (
// ChunkSizeOfChunkedListUserEmail chunk size of calling slackRepository.ListUsersEmail
ChunkSizeOfChunkedListUserEmail = 20
)

// chunkedListUsersEmail splits userID array into chunks,
// and calls slackRepository.ListUsersEmail for each chunk.
func (s *slackReactionUsersService) chunkedListUsersEmail(ctx context.Context, userIDs []string) ([]*model.SlackUserEmail, error) {
chunkedUserIDsList := slice.SplitStringSliceInChunks(userIDs, ChunkSizeOfChunkedListUserEmail)
slackUserEmails := make([]*model.SlackUserEmail, 0, len(chunkedUserIDsList))
for _, chunkedUserIDs := range chunkedUserIDsList {
userEmails, err := s.slackRepository.ListUsersEmail(ctx, chunkedUserIDs)
if err != nil {
return nil, err
}
slackUserEmails = append(slackUserEmails, userEmails...)
}
return slackUserEmails, nil
}

func (s *slackReactionUsersService) ListUsersEmailByReaction(ctx context.Context, channelID, ts, reactionName string) ([]*model.SlackUserEmail, error) {
msg, err := s.slackRepository.GetParentMessage(ctx, channelID, ts)
if err != nil {
return nil, err
}
inviteUserIDs := s.getReactionUserIDs(ctx, msg.Reactions, reactionName)
inviteUserEmails, err := s.slackRepository.ListUsersEmail(ctx, inviteUserIDs)
inviteUserEmails, err := s.chunkedListUsersEmail(ctx, inviteUserIDs)
if err != nil {
return nil, err
}
Expand Down
59 changes: 43 additions & 16 deletions app/internal/domain/service/slack_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package service
import (
"context"
"fmt"
"strings"

"github.com/moneyforward/auriga/app/pkg/slice"

"github.com/moneyforward/auriga/app/internal/domain/repository"
"github.com/moneyforward/auriga/app/internal/model"
Expand All @@ -31,34 +34,58 @@ type SlackResponseService interface {
ReplyHelp(ctx context.Context, event *slackevents.AppMentionEvent) error
}

type slackErrorResponseService struct {
type slackResponseService struct {
slackRepository repository.SlackRepository
errorRepository repository.ErrorRepository
}

func NewSlackResponseService(factory repository.Factory) *slackErrorResponseService {
return &slackErrorResponseService{
func NewSlackResponseService(factory repository.Factory) *slackResponseService {
return &slackResponseService{
slackRepository: factory.SlackRepository(),
errorRepository: factory.ErrorRepository(),
}
}

func (s *slackErrorResponseService) ReplyEmailList(ctx context.Context, event *slackevents.AppMentionEvent, emails []*model.SlackUserEmail) error {
msg := "参加者一覧\n"
const (
// lineSizeOfPostEmailList is limit number of line when Auriga reply Email List message.
// According to Slack API Documentation, the limit number of characters of postMessageAPI is 4000 for the best results.
// If the average length of email addresses sent at one time exceeds approximately 80, there is a risk of error within this method.
// but, it is the number we can afford.
lineSizeOfPostEmailList = 50
)

// postEmailList method posts emailList using slack postMessageAPI.
// The chunkedLines are generated and requested for each chunk,
// because of considering the limit the number of characters of slackAPI.
func (s *slackResponseService) postEmailList(ctx context.Context, channelID string, emails []*model.SlackUserEmail, ts string) error {
lines := append(make([]string, 0, len(emails)+1), "参加者一覧")
for _, email := range emails {
msg += email.Email
msg += "\n"
lines = append(lines, email.Email)
}
err := s.slackRepository.PostMessage(
ctx,
event.Channel,
fmt.Sprint(msg),
event.ThreadTimeStamp,
)
return err
chunkedLines := slice.SplitStringSliceInChunks(lines, lineSizeOfPostEmailList)
for _, chunkedLine := range chunkedLines {
err := s.slackRepository.PostMessage(ctx, channelID, strings.Join(chunkedLine, "\n"), ts)
if err != nil {
return err
}
}
return nil
}

func (s *slackResponseService) ReplyEmailList(ctx context.Context, event *slackevents.AppMentionEvent, emails []*model.SlackUserEmail) error {
if len(emails) <= lineSizeOfPostEmailList-1 {
var b strings.Builder
b.WriteString("参加者一覧")
for _, email := range emails {
b.WriteString("\n" + email.Email)
}
return s.slackRepository.PostMessage(ctx, event.Channel, b.String(), event.ThreadTimeStamp)
}

return s.postEmailList(ctx, event.Channel, emails, event.ThreadTimeStamp)
}

func (s *slackErrorResponseService) ReplyError(ctx context.Context, event *slackevents.AppMentionEvent, err error) error {
func (s *slackResponseService) ReplyError(ctx context.Context, event *slackevents.AppMentionEvent, err error) error {
var msg string
if s.errorRepository.ErrThreadNotFound(err) {
msg += "スレッドで呼び出してね:neko_namida:"
Expand All @@ -75,7 +102,7 @@ func (s *slackErrorResponseService) ReplyError(ctx context.Context, event *slack
return err
}

func (s *slackErrorResponseService) ReplyHelp(ctx context.Context, event *slackevents.AppMentionEvent) error {
func (s *slackResponseService) ReplyHelp(ctx context.Context, event *slackevents.AppMentionEvent) error {
msg := "[使い方]\n" +
"1. スレッドで `@Auriga :sanka:` のようにAurigaを呼び出し、リアクションを指定してください。\n" +
"2. スレッドの開始メッセージに指定のリアクションをしたユーザのメールアドレス一覧を返します。\n" +
Expand Down
121 changes: 116 additions & 5 deletions app/internal/domain/service/slack_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package service
import (
"context"
"errors"
"fmt"
"strings"
"testing"

"github.com/golang/mock/gomock"
Expand All @@ -28,6 +30,115 @@ import (
"github.com/slack-go/slack/slackevents"
)

func createEmails(suffixBase, n int) []*model.SlackUserEmail {
emails := make([]*model.SlackUserEmail, n)
for index := range emails {
emails[index] = &model.SlackUserEmail{
Email: fmt.Sprintf("user_%[email protected]", suffixBase+index),
}
}
return emails
}

func createMessage(base string, emails []*model.SlackUserEmail) string {
emailStrings := convertEmailsToStrings(emails)
if base != "" {
emailStrings = append([]string{base}, emailStrings...)
}
return strings.Join(emailStrings, "\n")
}

func convertEmailsToStrings(emails []*model.SlackUserEmail) []string {
es := make([]string, len(emails))
for index, email := range emails {
es[index] = email.Email
}
return es
}

func Test_slackResponseService_postEmailList(t *testing.T) {

type args struct {
emails []*model.SlackUserEmail
cid string
ts string
}
tests := []struct {
name string
args args
prepare func(msr *mock_repository.MockSlackRepository)
wantErr bool
}{
{
name: "OK: number of emails = 1",
args: args{
emails: createEmails(0, 1),
ts: "ts",
cid: "cid",
},
prepare: func(msr *mock_repository.MockSlackRepository) {
gomock.InOrder(
msr.EXPECT().PostMessage(
gomock.Any(), "cid", createMessage("参加者一覧", createEmails(0, 1)), "ts").
Return(nil),
)
},
},
{
name: "OK: number of emails = lineSizeOfPostEmailList - 1",
args: args{
emails: createEmails(0, lineSizeOfPostEmailList-1),
ts: "ts",
cid: "cid",
},
prepare: func(msr *mock_repository.MockSlackRepository) {
gomock.InOrder(
msr.EXPECT().PostMessage(
gomock.Any(), "cid", createMessage("参加者一覧", createEmails(0, lineSizeOfPostEmailList-1)), "ts").
Return(nil),
)
},
},
{
name: "OK: number of emails = lineSizeOfPostEmailList",
args: args{
emails: createEmails(0, lineSizeOfPostEmailList),
ts: "ts",
cid: "cid",
},
prepare: func(msr *mock_repository.MockSlackRepository) {
gomock.InOrder(
msr.EXPECT().PostMessage(
gomock.Any(), "cid", createMessage("参加者一覧", createEmails(0, lineSizeOfPostEmailList-1)), "ts").
Return(nil),
msr.EXPECT().PostMessage(
gomock.Any(), "cid", createMessage("", createEmails(lineSizeOfPostEmailList-1, 1)), "ts").
Return(nil),
)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
msr := mock_repository.NewMockSlackRepository(ctrl)
mer := mock_repository.NewMockErrorRepository(ctrl)
if tt.prepare != nil {
tt.prepare(msr)
}
s := &slackResponseService{
slackRepository: msr,
errorRepository: mer,
}
if err := s.postEmailList(ctx, tt.args.cid, tt.args.emails, tt.args.ts); (err != nil) != tt.wantErr {
t.Errorf("postEmailList() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func Test_slackErrorResponseService_ReplyEmailList(t *testing.T) {
type args struct {
event *slackevents.AppMentionEvent
Expand All @@ -53,7 +164,7 @@ func Test_slackErrorResponseService_ReplyEmailList(t *testing.T) {
},
prepare: func(msr *mock_repository.MockSlackRepository) {
msr.EXPECT().PostMessage(gomock.Any(), "sampleChannel",
"参加者一覧\n[email protected]\n[email protected]\n",
"参加者一覧\n[email protected]\n[email protected]",
"sampleThreadTimeStamp").Return(nil)
},
},
Expand All @@ -71,7 +182,7 @@ func Test_slackErrorResponseService_ReplyEmailList(t *testing.T) {
},
prepare: func(msr *mock_repository.MockSlackRepository) {
msr.EXPECT().PostMessage(gomock.Any(), "sampleChannel",
"参加者一覧\n[email protected]\n[email protected]\n",
"参加者一覧\n[email protected]\n[email protected]",
"sampleThreadTimeStamp").Return(errors.New("sample error"))
},
wantErr: true,
Expand All @@ -86,7 +197,7 @@ func Test_slackErrorResponseService_ReplyEmailList(t *testing.T) {
if tt.prepare != nil {
tt.prepare(msr)
}
s := &slackErrorResponseService{
s := &slackResponseService{
slackRepository: msr,
errorRepository: mer,
}
Expand Down Expand Up @@ -216,7 +327,7 @@ func Test_slackErrorResponseService_ReplyError(t *testing.T) {
if tt.prepare != nil {
tt.prepare(mer, msr)
}
s := &slackErrorResponseService{
s := &slackResponseService{
slackRepository: msr,
errorRepository: mer,
}
Expand Down Expand Up @@ -286,7 +397,7 @@ func Test_slackErrorResponseService_ReplyHelp(t *testing.T) {
if tt.prepare != nil {
tt.prepare(mer, msr)
}
s := &slackErrorResponseService{
s := &slackResponseService{
slackRepository: msr,
errorRepository: mer,
}
Expand Down
15 changes: 15 additions & 0 deletions app/pkg/slice/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,18 @@ func ToStringSet(s []string) []string {
}
return set
}

// SplitStringSliceInChunks SplitStringSliceInChunk split slice in chunk
// if you set chunkSize is less than 1, this function returns a 2 dimensional slice with 1 chunk ([][]string{s}).
// TODO: Update 1.18+ and use generics
func SplitStringSliceInChunks(s []string, chunkSize int) [][]string {
if chunkSize < 1 {
return [][]string{s}
}
chunks := make([][]string, 0, len(s)/chunkSize+1)
for chunkSize < len(s) {
chunks = append(chunks, s[:chunkSize])
s = s[chunkSize:]
}
return append(chunks, s)
}
Loading

0 comments on commit cb23816

Please sign in to comment.